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