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