001/*
002 * (C) Copyright 2006-2010 Nuxeo SA (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 *     bstefanescu
018 */
019package org.nuxeo.runtime.reload;
020
021import java.io.File;
022import java.io.IOException;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.util.Arrays;
026import java.util.jar.Manifest;
027
028import javax.transaction.Transaction;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.common.Environment;
033import org.nuxeo.common.utils.FileUtils;
034import org.nuxeo.common.utils.JarUtils;
035import org.nuxeo.common.utils.ZipUtils;
036import org.nuxeo.runtime.RuntimeService;
037import org.nuxeo.runtime.RuntimeServiceException;
038import org.nuxeo.runtime.api.Framework;
039import org.nuxeo.runtime.api.ServicePassivator;
040import org.nuxeo.runtime.deployment.preprocessor.DeploymentPreprocessor;
041import org.nuxeo.runtime.model.ComponentContext;
042import org.nuxeo.runtime.model.DefaultComponent;
043import org.nuxeo.runtime.services.event.Event;
044import org.nuxeo.runtime.services.event.EventService;
045import org.nuxeo.runtime.transaction.TransactionHelper;
046import org.osgi.framework.Bundle;
047import org.osgi.framework.BundleContext;
048import org.osgi.framework.BundleException;
049import org.osgi.framework.ServiceReference;
050import org.osgi.service.packageadmin.PackageAdmin;
051
052/**
053 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
054 */
055public class ReloadComponent extends DefaultComponent implements ReloadService {
056
057    private static final Log log = LogFactory.getLog(ReloadComponent.class);
058
059    protected static Bundle bundle;
060
061    protected Long lastFlushed;
062
063    public static BundleContext getBundleContext() {
064        return bundle.getBundleContext();
065    }
066
067    public static Bundle getBundle() {
068        return bundle;
069    }
070
071    @Override
072    public void activate(ComponentContext context) {
073        super.activate(context);
074        bundle = context.getRuntimeContext().getBundle();
075    }
076
077    @Override
078    public void deactivate(ComponentContext context) {
079        super.deactivate(context);
080        bundle = null;
081    }
082
083    @Override
084    public void reload() {
085        if (log.isDebugEnabled()) {
086            log.debug("Starting reload");
087        }
088        try {
089            reloadProperties();
090        } catch (IOException e) {
091            throw new RuntimeServiceException(e);
092        }
093        triggerReloadWithNewTransaction(RELOAD_EVENT_ID);
094    }
095
096    @Override
097    public void reloadProperties() throws IOException {
098        log.info("Reload runtime properties");
099        Framework.getRuntime().reloadProperties();
100    }
101
102    @Override
103    public void reloadRepository() {
104        log.info("Reload repository");
105        triggerReloadWithNewTransaction(RELOAD_REPOSITORIES_ID);
106    }
107
108    @Override
109    public void reloadSeamComponents() {
110        log.info("Reload Seam components");
111        triggerReload(RELOAD_SEAM_EVENT_ID);
112    }
113
114    @Override
115    public void flush() {
116        log.info("Flush caches");
117        triggerReloadWithNewTransaction(FLUSH_EVENT_ID);
118    }
119
120    @Override
121    public void flushJaasCache() {
122        log.info("Flush the JAAS cache");
123        Framework.getLocalService(EventService.class)
124                .sendEvent(new Event("usermanager", "user_changed", this, "Deployer"));
125        setFlushedNow();
126    }
127
128    @Override
129    public void flushSeamComponents() {
130        log.info("Flush Seam components");
131        triggerReload(FLUSH_SEAM_EVENT_ID);
132    }
133
134    @Override
135    public String deployBundle(File file) throws BundleException {
136        return deployBundle(file, false);
137    }
138
139    @Override
140    public String deployBundle(File file, boolean reloadResourceClasspath) throws BundleException {
141        String name = getOSGIBundleName(file);
142        if (name == null) {
143            log.error(
144                    String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", file.getAbsolutePath()));
145            return null;
146        }
147
148        String path = file.getAbsolutePath();
149
150        log.info(String.format("Before deploy bundle for file at '%s'\n" + "%s", path, getRuntimeStatus()));
151
152        if (reloadResourceClasspath) {
153            URL url;
154            try {
155                url = new File(path).toURI().toURL();
156            } catch (MalformedURLException e) {
157                throw new RuntimeException(e);
158            }
159            Framework.reloadResourceLoader(Arrays.asList(url), null);
160        }
161
162        // check if this is a bundle first
163        Bundle newBundle = getBundleContext().installBundle(path);
164        if (newBundle == null) {
165            throw new IllegalArgumentException("Could not find a valid bundle at path: " + path);
166        }
167        Transaction tx = TransactionHelper.suspendTransaction();
168        try {
169            newBundle.start();
170        } finally {
171            TransactionHelper.resumeTransaction(tx);
172        }
173
174        log.info(String.format("Deploy done for bundle with name '%s'.\n" + "%s", newBundle.getSymbolicName(),
175                getRuntimeStatus()));
176
177        return newBundle.getSymbolicName();
178    }
179
180    @Override
181    public void undeployBundle(File file, boolean reloadResources) throws BundleException {
182        String name = getOSGIBundleName(file);
183        String path = file.getAbsolutePath();
184        if (name == null) {
185            log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", path));
186            return;
187        }
188
189        undeployBundle(name);
190
191        if (reloadResources) {
192            URL url;
193            try {
194                url = new File(path).toURI().toURL();
195            } catch (MalformedURLException e) {
196                throw new RuntimeException(e);
197            }
198            Framework.reloadResourceLoader(null, Arrays.asList(url));
199        }
200    }
201
202    @Override
203    public void undeployBundle(String bundleName) throws BundleException {
204        if (bundleName == null) {
205            // ignore
206            return;
207        }
208        log.info(String.format("Before undeploy bundle with name '%s'.\n" + "%s", bundleName, getRuntimeStatus()));
209        BundleContext ctx = getBundleContext();
210        ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName());
211        PackageAdmin srv = (PackageAdmin) ctx.getService(ref);
212        try {
213            for (Bundle b : srv.getBundles(bundleName, null)) {
214                if (b != null && b.getState() == Bundle.ACTIVE) {
215                    Transaction tx = TransactionHelper.suspendTransaction();
216                    try {
217                        b.stop();
218                        b.uninstall();
219                    } finally {
220                        TransactionHelper.resumeTransaction(tx);
221                    }
222                }
223            }
224        } finally {
225            ctx.ungetService(ref);
226        }
227        log.info(String.format("Undeploy done.\n" + "%s", getRuntimeStatus()));
228    }
229
230    @Override
231    public Long lastFlushed() {
232        return lastFlushed;
233    }
234
235    /**
236     * Sets the last date date to current date timestamp
237     *
238     * @since 5.6
239     */
240    protected void setFlushedNow() {
241        lastFlushed = Long.valueOf(System.currentTimeMillis());
242    }
243
244    /**
245     * @deprecated since 5.6, use {@link #runDeploymentPreprocessor()} instead
246     */
247    @Override
248    @Deprecated
249    public void installWebResources(File file) throws IOException {
250        log.info("Install web resources");
251        if (file.isDirectory()) {
252            File war = new File(file, "web");
253            war = new File(war, "nuxeo.war");
254            if (war.isDirectory()) {
255                FileUtils.copyTree(war, getAppDir());
256            } else {
257                // compatibility mode with studio 1.5 - see NXP-6186
258                war = new File(file, "nuxeo.war");
259                if (war.isDirectory()) {
260                    FileUtils.copyTree(war, getAppDir());
261                }
262            }
263        } else if (file.isFile()) { // a jar
264            File war = getWarDir();
265            ZipUtils.unzip("web/nuxeo.war", file, war);
266            // compatibility mode with studio 1.5 - see NXP-6186
267            ZipUtils.unzip("nuxeo.war", file, war);
268        }
269    }
270
271    @Override
272    public void runDeploymentPreprocessor() throws IOException {
273        if (log.isDebugEnabled()) {
274            log.debug("Start running deployment preprocessor");
275        }
276        String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath();
277        File root = new File(rootPath);
278        DeploymentPreprocessor processor = new DeploymentPreprocessor(root);
279        // initialize
280        processor.init();
281        // and predeploy
282        processor.predeploy();
283        if (log.isDebugEnabled()) {
284            log.debug("Deployment preprocessing done");
285        }
286    }
287
288    protected static File getAppDir() {
289        return Environment.getDefault().getConfig().getParentFile();
290    }
291
292    protected static File getWarDir() {
293        return new File(getAppDir(), "nuxeo.war");
294    }
295
296    @Override
297    public String getOSGIBundleName(File file) {
298        Manifest mf = JarUtils.getManifest(file);
299        if (mf == null) {
300            return null;
301        }
302        String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName");
303        if (bundleName == null) {
304            return null;
305        }
306        int index = bundleName.indexOf(';');
307        if (index > -1) {
308            bundleName = bundleName.substring(0, index);
309        }
310        return bundleName;
311    }
312
313    protected String getRuntimeStatus() {
314        StringBuilder msg = new StringBuilder();
315        RuntimeService runtime = Framework.getRuntime();
316        runtime.getStatusMessage(msg);
317        return msg.toString();
318    }
319
320    protected void triggerReload(String id) {
321        if (id.equals(RELOAD_SEAM_EVENT_ID)) {
322            log.info("about to send " + id);
323            Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, id, this, null));
324            return;
325        }
326        log.info("about to passivate for " + id);
327        Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, "before-reload", this, null));
328        try {
329            ServicePassivator
330                .proceed(() -> {
331                    log.info("about to send " + id);
332                    Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, id, this, null));
333                    if (id.startsWith(FLUSH_EVENT_ID)) {
334                        setFlushedNow();
335                    }
336                }).onFailure(snapshot -> {
337                    throw new UnsupportedOperationException("Detected access, should initiate a reboot " + snapshot.toString());
338                });
339        } finally {
340            Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, "after-reload", this, null));
341            log.info("returning from " + id);
342        }
343    }
344
345    protected void triggerReloadWithNewTransaction(String id) {
346        if (TransactionHelper.isTransactionMarkedRollback()) {
347            throw new AssertionError("The calling transaction is marked rollback");
348        } else if (TransactionHelper.isTransactionActive()) { // should flush
349                                                              // the calling
350                                                              // transaction
351            TransactionHelper.commitOrRollbackTransaction();
352            TransactionHelper.startTransaction();
353        }
354        try {
355            try {
356                triggerReload(id);
357            } catch (RuntimeException cause) {
358                TransactionHelper.setTransactionRollbackOnly();
359                throw cause;
360            }
361        } finally {
362            if (TransactionHelper.isTransactionActiveOrMarkedRollback()) {
363                boolean wasRollbacked = TransactionHelper.isTransactionMarkedRollback();
364                TransactionHelper.commitOrRollbackTransaction();
365                TransactionHelper.startTransaction();
366                if (wasRollbacked) {
367                    TransactionHelper.setTransactionRollbackOnly();
368                }
369            }
370        }
371    }
372}