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