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