001/*
002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 */
019package org.nuxeo.runtime;
020
021import java.io.File;
022import java.io.IOException;
023import java.net.URL;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.logging.Level;
032import java.util.stream.Collectors;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.common.codec.CryptoProperties;
037import org.nuxeo.common.logging.JavaUtilLoggingHelper;
038import org.nuxeo.common.logging.Log4JHelper;
039import org.nuxeo.common.logging.Log4jWatchdogHandle;
040import org.nuxeo.common.utils.TextTemplate;
041import org.nuxeo.runtime.api.Framework;
042import org.nuxeo.runtime.model.ComponentInstance;
043import org.nuxeo.runtime.model.ComponentManager;
044import org.nuxeo.runtime.model.ComponentName;
045import org.nuxeo.runtime.model.Extension;
046import org.nuxeo.runtime.model.RuntimeContext;
047import org.nuxeo.runtime.model.impl.ComponentManagerImpl;
048import org.nuxeo.runtime.model.impl.DefaultRuntimeContext;
049import org.osgi.framework.Bundle;
050
051/**
052 * Abstract implementation of the Runtime Service.
053 * <p>
054 * Implementors are encouraged to extend this class instead of directly implementing the {@link RuntimeService}
055 * interface.
056 *
057 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
058 */
059public abstract class AbstractRuntimeService implements RuntimeService {
060
061    /**
062     * Property that controls whether or not to redirect JUL to JCL. By default is true (JUL will be redirected)
063     */
064    public static final String REDIRECT_JUL = "org.nuxeo.runtime.redirectJUL";
065
066    public static final String REDIRECT_JUL_THRESHOLD = "org.nuxeo.runtime.redirectJUL.threshold";
067
068    public static final String LOG4J_WATCH_DISABLED = "org.nuxeo.runtime.log4jwatch.disabled";
069
070    public static final String LOG4J_WATCH_DELAY = "org.nuxeo.runtime.log4jwatch.delay";
071
072    public static final long LOG4J_WATCH_DELAY_DEFAULT = 10;
073
074    // package-private for subclass access without synthetic accessor
075    static final Log log = LogFactory.getLog(RuntimeService.class);
076
077    protected boolean isStarted = false;
078
079    protected boolean isShuttingDown = false;
080
081    protected File workingDir;
082
083    protected CryptoProperties properties = new CryptoProperties(System.getProperties());
084
085    protected ComponentManager manager;
086
087    protected final RuntimeContext context;
088
089    protected AbstractRuntimeService(DefaultRuntimeContext context) {
090        this(context, null);
091    }
092
093    /**
094     * Warnings during the deployment. These messages don't block startup, even in strict mode.
095     */
096    protected final List<String> warnings = new ArrayList<>();
097
098    protected LogConfig logConfig = new LogConfig();
099
100    /**
101     * Errors during the deployment. Here are collected all errors occurred during the startup. These messages block
102     * startup in strict mode.
103     *
104     * @since 9.1
105     */
106    protected final List<String> errors = new ArrayList<>();
107
108    protected AbstractRuntimeService(DefaultRuntimeContext context, Map<String, String> properties) {
109        this.context = context;
110        context.setRuntime(this);
111        if (properties != null) {
112            this.properties.putAll(properties);
113        }
114        // get errors set by NuxeoDeployer
115        String errs = System.getProperty("org.nuxeo.runtime.deployment.errors");
116        if (errs != null) {
117            errors.addAll(Arrays.asList(errs.split("\n")));
118            System.clearProperty("org.nuxeo.runtime.deployment.errors");
119        }
120    }
121
122    @Override
123    public List<String> getWarnings() {
124        return warnings;
125    }
126
127    /**
128     * @since 9.1
129     */
130    @Override
131    public List<String> getErrors() {
132        return errors;
133    }
134
135    protected ComponentManager createComponentManager() {
136        return new ComponentManagerImpl(this);
137    }
138
139    protected static URL getBuiltinFeatureURL() {
140        return Thread.currentThread().getContextClassLoader().getResource("org/nuxeo/runtime/nx-feature.xml");
141    }
142
143    @Override
144    public synchronized void start() {
145        if (isStarted) {
146            return;
147        }
148
149        manager = createComponentManager();
150        try {
151            loadConfig();
152        } catch (IOException e) {
153            throw new RuntimeServiceException(e);
154        }
155
156        logConfig.configure();
157
158        log.info("Starting Nuxeo Runtime service " + getName() + "; version: " + getVersion());
159
160        Framework.sendEvent(new RuntimeServiceEvent(RuntimeServiceEvent.RUNTIME_ABOUT_TO_START, this));
161        try {
162            doStart();
163        } finally {
164            Framework.sendEvent(
165                    new RuntimeServiceEvent(RuntimeServiceEvent.RUNTIME_STARTED, this));
166            isStarted = true;
167        }
168    }
169
170    @Override
171    public synchronized void stop() {
172        if (!isStarted) {
173            return;
174        }
175        isShuttingDown = true;
176        try {
177            log.info("Stopping Nuxeo Runtime service " + getName() + "; version: " + getVersion());
178            Framework.sendEvent(new RuntimeServiceEvent(RuntimeServiceEvent.RUNTIME_ABOUT_TO_STOP, this));
179            try {
180                manager.shutdown();
181                doStop();
182            } finally {
183                isStarted = false;
184                Framework.sendEvent(
185                        new RuntimeServiceEvent(RuntimeServiceEvent.RUNTIME_STOPPED, this));
186                manager = null;
187            }
188        } finally {
189            logConfig.cleanup();
190            isShuttingDown = false;
191        }
192    }
193
194    @Override
195    public boolean isStarted() {
196        return isStarted;
197    }
198
199    @Override
200    public boolean isShuttingDown() {
201        return isShuttingDown;
202    }
203
204    protected void loadConfig() throws IOException {
205    }
206
207    protected void doStart() {
208    }
209
210    protected void doStop() {
211    }
212
213    @Override
214    public File getHome() {
215        return workingDir;
216    }
217
218    public void setHome(File home) {
219        workingDir = home;
220    }
221
222    @Override
223    public String getDescription() {
224        return toString();
225    }
226
227    @Override
228    public CryptoProperties getProperties() {
229        // do not unreference properties: some methods rely on this to set
230        // variables here...
231        return properties;
232    }
233
234    @Override
235    public String getProperty(String name) {
236        return getProperty(name, null);
237    }
238
239    @Override
240    public String getProperty(String name, String defValue) {
241        String value = properties.getProperty(name, defValue);
242        if (value == null || ("${" + name + "}").equals(value)) {
243            // avoid loop, don't expand
244            return value;
245        }
246        return expandVars(value);
247    }
248
249    @Override
250    public void setProperty(String name, Object value) {
251        properties.setProperty(name, value.toString());
252    }
253
254    @Override
255    public String toString() {
256        return getName() + " version " + getVersion();
257    }
258
259    @Override
260    public Object getComponent(String name) {
261        ComponentInstance co = getComponentInstance(name);
262        return co != null ? co.getInstance() : null;
263    }
264
265    @Override
266    public Object getComponent(ComponentName name) {
267        ComponentInstance co = getComponentInstance(name);
268        return co != null ? co.getInstance() : null;
269    }
270
271    @Override
272    public ComponentInstance getComponentInstance(String name) {
273        return manager.getComponent(new ComponentName(name));
274    }
275
276    @Override
277    public ComponentInstance getComponentInstance(ComponentName name) {
278        return manager.getComponent(name);
279    }
280
281    @Override
282    public ComponentManager getComponentManager() {
283        return manager;
284    }
285
286    @Override
287    public RuntimeContext getContext() {
288        return context;
289    }
290
291    @Override
292    public <T> T getService(Class<T> serviceClass) {
293        return manager.getService(serviceClass);
294    }
295
296    @Override
297    public String expandVars(String expression) {
298        return new TextTemplate(properties).processText(expression);
299    }
300
301    @Override
302    public File getBundleFile(Bundle bundle) {
303        return null;
304    }
305
306    @Override
307    public Bundle getBundle(String symbolicName) {
308        throw new UnsupportedOperationException("Not implemented");
309    }
310
311    /**
312     * @since 5.5
313     * @param msg summary message about all components loading status
314     * @return true if there was no detected error, else return false
315     */
316    @Override
317    public boolean getStatusMessage(StringBuilder msg) {
318        String hr = "======================================================================";
319        if (!warnings.isEmpty()) {
320            msg.append(hr).append("\n= Component Loading Warnings:\n");
321            for (String warning : warnings) {
322                msg.append("  * ").append(warning).append('\n');
323            }
324        }
325        if (!errors.isEmpty()) {
326            msg.append(hr).append("\n= Component Loading Errors:\n");
327            for (String error : errors) {
328                msg.append("  * ").append(error).append('\n');
329            }
330        }
331        Map<ComponentName, Set<ComponentName>> pendingRegistrations = manager.getPendingRegistrations();
332        Map<ComponentName, Set<Extension>> missingRegistrations = manager.getMissingRegistrations();
333        Collection<ComponentName> unstartedRegistrations = manager.getActivatingRegistrations();
334        unstartedRegistrations.addAll(manager.getStartFailureRegistrations());
335        msg.append(hr)
336                .append("\n= Component Loading Status: Pending: ")
337                .append(pendingRegistrations.size())
338           .append(" / Missing: ")
339           .append(missingRegistrations.size())
340                .append(" / Unstarted: ")
341                .append(unstartedRegistrations.size())
342                .append(" / Total: ")
343           .append(manager.getRegistrations().size())
344                .append('\n');
345        for (Entry<ComponentName, Set<ComponentName>> e : pendingRegistrations.entrySet()) {
346            msg.append("  * ").append(e.getKey()).append(" requires ").append(e.getValue()).append('\n');
347        }
348        for (Entry<ComponentName, Set<Extension>> e : missingRegistrations.entrySet()) {
349            msg.append("  * ")
350                    .append(e.getKey())
351               .append(" references missing ")
352               .append(e.getValue()
353                        .stream()
354                        .map(ext -> "target=" + ext.getTargetComponent().getName() + ";point="
355                                + ext.getExtensionPoint())
356                        .collect(Collectors.toList()))
357                    .append('\n');
358        }
359        for (ComponentName componentName : unstartedRegistrations) {
360            msg.append("  - ").append(componentName).append('\n');
361        }
362        msg.append(hr);
363        return (errors.isEmpty() && pendingRegistrations.isEmpty() && missingRegistrations.isEmpty()
364                && unstartedRegistrations.isEmpty());
365    }
366
367    /**
368     * Error logger which does its logging from a separate thread, for thread isolation.
369     *
370     * @param message the message to log
371     * @return a thread that can be started to do the logging
372     * @since 9.2, 8.10-HF05
373     */
374    public static Thread getErrorLoggerThread(String message) {
375        return new Thread() {
376            @Override
377            public void run() {
378                log.error(message);
379            }
380        };
381    }
382
383    /**
384     * Configure the logging system (log4j) at runtime startup and do any cleanup is needed when the runtime is stopped
385     */
386    protected class LogConfig {
387
388        Log4jWatchdogHandle wdog;
389
390        public void configure(){
391            if (Boolean.parseBoolean(getProperty(REDIRECT_JUL, "true"))) {
392                Level threshold = Level.parse(getProperty(REDIRECT_JUL_THRESHOLD, "INFO").toUpperCase());
393                JavaUtilLoggingHelper.redirectToApacheCommons(threshold);
394            }
395            if (Boolean.parseBoolean(getProperty(LOG4J_WATCH_DISABLED, "false"))) {
396                log.info("Disabled log4j.xml change detection");
397            } else {
398                long delay;
399                try {
400                    delay = Long.parseLong(getProperty(LOG4J_WATCH_DELAY, Long.toString(LOG4J_WATCH_DELAY_DEFAULT)));
401                } catch (NumberFormatException e) {
402                    delay = LOG4J_WATCH_DELAY_DEFAULT;
403                }
404                wdog = Log4JHelper.configureAndWatch(delay);
405                if (wdog == null) {
406                    log.warn("Failed to configure log4j.xml change detection");
407                } else {
408                    log.info("Configured log4j.xml change detection with a delay of " + delay + "s");
409                }
410            }
411        }
412
413        public void cleanup() {
414            try {
415                if (wdog != null) {
416                    wdog.cancel();
417                }
418            } finally {
419                wdog = null;
420                JavaUtilLoggingHelper.reset();
421            }
422        }
423    }
424
425}