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 *     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.Collections;
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.deployment.preprocessor.DeploymentPreprocessor;
040import org.nuxeo.runtime.model.ComponentContext;
041import org.nuxeo.runtime.model.ComponentManager;
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    protected void refreshComponents() {
084        String reloadStrategy = Framework.getProperty("org.nuxeo.runtime.reload_strategy", "restart");
085        log.info("Refresh components. Strategy: " + reloadStrategy);
086        // reload components / contributions
087        ComponentManager mgr = Framework.getRuntime().getComponentManager();
088        if ("unstash".equals(reloadStrategy)) {
089            // compat mode
090            mgr.unstash();
091        } else if ("standby".equals(reloadStrategy)) { // standby / resume
092            mgr.standby();
093            mgr.unstash();
094            mgr.resume();
095        } else { // restart mode
096            mgr.refresh(false);
097        }
098    }
099
100    @Override
101    public void reload() throws InterruptedException {
102        if (log.isDebugEnabled()) {
103            log.debug("Starting reload");
104        }
105
106        try {
107            reloadProperties();
108        } catch (IOException e) {
109            throw new RuntimeServiceException(e);
110        }
111
112        triggerReloadWithNewTransaction(RELOAD_EVENT_ID);
113    }
114
115    @Override
116    public void reloadProperties() throws IOException {
117        log.info("Reload runtime properties");
118        Framework.getRuntime().reloadProperties();
119    }
120
121    @Override
122    public void reloadRepository() throws InterruptedException {
123        log.info("Reload repository");
124        triggerReloadWithNewTransaction(RELOAD_REPOSITORIES_ID);
125    }
126
127    @Override
128    public void reloadSeamComponents() {
129        log.info("Reload Seam components");
130        Framework.getLocalService(EventService.class)
131                 .sendEvent(new Event(RELOAD_TOPIC, RELOAD_SEAM_EVENT_ID, this, null));
132    }
133
134    @Override
135    public void flush() {
136        log.info("Flush caches");
137        Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null));
138        flushJaasCache();
139        setFlushedNow();
140    }
141
142    @Override
143    public void flushJaasCache() {
144        log.info("Flush the JAAS cache");
145        Framework.getLocalService(EventService.class)
146                 .sendEvent(new Event("usermanager", "user_changed", this, "Deployer"));
147        setFlushedNow();
148    }
149
150    @Override
151    public void flushSeamComponents() {
152        log.info("Flush Seam components");
153        Framework.getLocalService(EventService.class)
154                 .sendEvent(new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null));
155        setFlushedNow();
156    }
157
158    @Override
159    public String deployBundle(File file) throws BundleException {
160        return deployBundle(file, false);
161    }
162
163    @Override
164    public String deployBundle(File file, boolean reloadResourceClasspath) throws BundleException {
165        String name = getOSGIBundleName(file);
166        if (name == null) {
167            log.error(
168                    String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", file.getAbsolutePath()));
169            return null;
170        }
171
172        String path = file.getAbsolutePath();
173
174        log.info(String.format("Before deploy bundle for file at '%s'\n" + "%s", path, getRuntimeStatus()));
175
176        if (reloadResourceClasspath) {
177            URL url;
178            try {
179                url = new File(path).toURI().toURL();
180            } catch (MalformedURLException e) {
181                throw new RuntimeException(e);
182            }
183            Framework.reloadResourceLoader(Collections.singletonList(url), null);
184        }
185
186        // check if this is a bundle first
187        Bundle newBundle = getBundleContext().installBundle(path);
188        if (newBundle == null) {
189            throw new IllegalArgumentException("Could not find a valid bundle at path: " + path);
190        }
191        Transaction tx = TransactionHelper.suspendTransaction();
192        try {
193            newBundle.start();
194            refreshComponents();
195        } finally {
196            TransactionHelper.resumeTransaction(tx);
197        }
198
199        log.info(String.format("Deploy done for bundle with name '%s'.\n" + "%s", newBundle.getSymbolicName(),
200                getRuntimeStatus()));
201
202        return newBundle.getSymbolicName();
203    }
204
205    @Override
206    public void undeployBundle(File file, boolean reloadResources) throws BundleException {
207        String name = getOSGIBundleName(file);
208        String path = file.getAbsolutePath();
209        if (name == null) {
210            log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", path));
211            return;
212        }
213
214        undeployBundle(name);
215
216        if (reloadResources) {
217            URL url;
218            try {
219                url = new File(path).toURI().toURL();
220            } catch (MalformedURLException e) {
221                throw new RuntimeException(e);
222            }
223            Framework.reloadResourceLoader(null, Collections.singletonList(url));
224        }
225    }
226
227    @Override
228    public void undeployBundle(String bundleName) throws BundleException {
229        if (bundleName == null) {
230            // ignore
231            return;
232        }
233        log.info(String.format("Before undeploy bundle with name '%s'.\n" + "%s", bundleName, getRuntimeStatus()));
234        BundleContext ctx = getBundleContext();
235        ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName());
236        PackageAdmin srv = (PackageAdmin) ctx.getService(ref);
237        try {
238            for (Bundle b : srv.getBundles(bundleName, null)) {
239                if (b != null && b.getState() == Bundle.ACTIVE) {
240                    Transaction tx = TransactionHelper.suspendTransaction();
241                    try {
242                        b.stop();
243                        b.uninstall();
244                        refreshComponents();
245                    } finally {
246                        TransactionHelper.resumeTransaction(tx);
247                    }
248                }
249            }
250        } finally {
251            ctx.ungetService(ref);
252        }
253        log.info(String.format("Undeploy done.\n" + "%s", getRuntimeStatus()));
254    }
255
256    @Override
257    public Long lastFlushed() {
258        return lastFlushed;
259    }
260
261    /**
262     * Sets the last date date to current date timestamp
263     *
264     * @since 5.6
265     */
266    protected void setFlushedNow() {
267        lastFlushed = System.currentTimeMillis();
268    }
269
270    /**
271     * @deprecated since 5.6, use {@link #runDeploymentPreprocessor()} instead.
272     *             Keep it as compatibility code until NXP-9642 is done.
273     */
274    @Override
275    @Deprecated
276    public void installWebResources(File file) throws IOException {
277        log.info("Install web resources");
278        if (file.isDirectory()) {
279            File war = new File(file, "web");
280            war = new File(war, "nuxeo.war");
281            if (war.isDirectory()) {
282                FileUtils.copyTree(war, getAppDir());
283            } else {
284                // compatibility mode with studio 1.5 - see NXP-6186
285                war = new File(file, "nuxeo.war");
286                if (war.isDirectory()) {
287                    FileUtils.copyTree(war, getAppDir());
288                }
289            }
290        } else if (file.isFile()) { // a jar
291            File war = getWarDir();
292            ZipUtils.unzip("web/nuxeo.war", file, war);
293            // compatibility mode with studio 1.5 - see NXP-6186
294            ZipUtils.unzip("nuxeo.war", file, war);
295        }
296    }
297
298    @Override
299    public void runDeploymentPreprocessor() throws IOException {
300        if (log.isDebugEnabled()) {
301            log.debug("Start running deployment preprocessor");
302        }
303        String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath();
304        File root = new File(rootPath);
305        DeploymentPreprocessor processor = new DeploymentPreprocessor(root);
306        // initialize
307        processor.init();
308        // and predeploy
309        processor.predeploy();
310        if (log.isDebugEnabled()) {
311            log.debug("Deployment preprocessing done");
312        }
313    }
314
315    protected static File getAppDir() {
316        return Environment.getDefault().getConfig().getParentFile();
317    }
318
319    protected static File getWarDir() {
320        return new File(getAppDir(), "nuxeo.war");
321    }
322
323    @Override
324    public String getOSGIBundleName(File file) {
325        Manifest mf = JarUtils.getManifest(file);
326        if (mf == null) {
327            return null;
328        }
329        String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName");
330        if (bundleName == null) {
331            return null;
332        }
333        int index = bundleName.indexOf(';');
334        if (index > -1) {
335            bundleName = bundleName.substring(0, index);
336        }
337        return bundleName;
338    }
339
340    protected String getRuntimeStatus() {
341        StringBuilder msg = new StringBuilder();
342        RuntimeService runtime = Framework.getRuntime();
343        runtime.getStatusMessage(msg);
344        return msg.toString();
345    }
346
347    protected void triggerReloadWithNewTransaction(String id) throws InterruptedException {
348        if (TransactionHelper.isTransactionMarkedRollback()) {
349            throw new AssertionError("The calling transaction is marked rollback");
350        }
351        // we need to commit or rollback transaction because suspending it leads to a lock/errors when acquiring a new
352        // connection during the datasource reload
353        TransactionHelper.commitOrRollbackTransaction();
354        TransactionHelper.startTransaction();
355        try {
356            try {
357                triggerReload(id);
358            } catch (RuntimeException cause) {
359                TransactionHelper.setTransactionRollbackOnly();
360                throw cause;
361            } finally {
362                TransactionHelper.commitOrRollbackTransaction();
363            }
364        } finally {
365            TransactionHelper.startTransaction();
366        }
367    }
368
369    protected void triggerReload(String id) throws InterruptedException {
370        log.info("about to reload for " + id);
371        Framework.getLocalService(EventService.class)
372                 .sendEvent(new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null));
373        try {
374            Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, id, this, null));
375        } finally {
376            Framework.getLocalService(EventService.class)
377                     .sendEvent(new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null));
378            log.info("returning from " + id);
379        }
380    }
381}