001/*
002 * (C) Copyright 2006-2010 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 */
017package org.nuxeo.runtime.reload;
018
019import java.io.File;
020import java.io.IOException;
021import java.net.MalformedURLException;
022import java.net.URL;
023import java.util.Arrays;
024import java.util.concurrent.CountDownLatch;
025import java.util.jar.Manifest;
026
027import javax.transaction.Transaction;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.common.Environment;
032import org.nuxeo.common.utils.FileUtils;
033import org.nuxeo.common.utils.JarUtils;
034import org.nuxeo.common.utils.ZipUtils;
035import org.nuxeo.runtime.RuntimeService;
036import org.nuxeo.runtime.RuntimeServiceException;
037import org.nuxeo.runtime.api.DefaultServiceProvider;
038import org.nuxeo.runtime.api.Framework;
039import org.nuxeo.runtime.api.ServiceProvider;
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).sendEvent(
124                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().getHome().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        final CountDownLatch reloadAchieved = new CountDownLatch(1);
322        final Thread ownerThread = Thread.currentThread();
323        try {
324            ServiceProvider next = DefaultServiceProvider.getProvider();
325            DefaultServiceProvider.setProvider(new ServiceProvider() {
326
327                @Override
328                public <T> T getService(Class<T> serviceClass) {
329                    if (Thread.currentThread() != ownerThread) {
330                        try {
331                            reloadAchieved.await();
332                        } catch (InterruptedException cause) {
333                            Thread.currentThread().interrupt();
334                            throw new AssertionError(serviceClass + "was interruped while waiting for reloading",
335                                    cause);
336                        }
337                    }
338                    if (next != null) {
339                        return next.getService(serviceClass);
340                    }
341                    return  Framework.getRuntime().getService(serviceClass);
342                }
343            });
344            try {
345                if (log.isDebugEnabled()) {
346                    log.debug("triggering reload("+id+")");
347                }
348                Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, id, this, null));
349                if (id.startsWith(FLUSH_EVENT_ID) || FLUSH_SEAM_EVENT_ID.equals(id)) {
350                    setFlushedNow();
351                }
352            } finally {
353                DefaultServiceProvider.setProvider(next);
354            }
355        } finally {
356            reloadAchieved.countDown();
357        }
358    }
359
360    protected void triggerReloadWithNewTransaction(String id) {
361        if (TransactionHelper.isTransactionMarkedRollback()) {
362            throw new AssertionError("The calling transaction is marked rollback=");
363        } else if (TransactionHelper.isTransactionActive()) { // should flush the calling transaction
364            TransactionHelper.commitOrRollbackTransaction();
365            TransactionHelper.startTransaction();
366        }
367        try {
368            try {
369                triggerReload(id);
370            } catch (RuntimeException cause) {
371                TransactionHelper.setTransactionRollbackOnly();
372                throw cause;
373            }
374        } finally {
375            if (TransactionHelper.isTransactionActiveOrMarkedRollback()) {
376                boolean wasRollbacked = TransactionHelper.isTransactionMarkedRollback();
377                TransactionHelper.commitOrRollbackTransaction();
378                TransactionHelper.startTransaction();
379                if (wasRollbacked) {
380                    TransactionHelper.setTransactionRollbackOnly();
381                }
382            }
383        }
384    }
385}