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.time.Duration;
026import java.util.Arrays;
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)
113                .sendEvent(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)
120                .sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null));
121        flushJaasCache();
122        setFlushedNow();
123    }
124
125    @Override
126    public void flushJaasCache() {
127        log.info("Flush the JAAS cache");
128        Framework.getLocalService(EventService.class)
129                .sendEvent(new Event("usermanager", "user_changed", this, "Deployer"));
130        setFlushedNow();
131    }
132
133    @Override
134    public void flushSeamComponents() {
135        log.info("Flush Seam components");
136        Framework.getLocalService(EventService.class)
137                .sendEvent(new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null));
138        setFlushedNow();
139    }
140
141    @Override
142    public String deployBundle(File file) throws BundleException {
143        return deployBundle(file, false);
144    }
145
146    @Override
147    public String deployBundle(File file, boolean reloadResourceClasspath) throws BundleException {
148        String name = getOSGIBundleName(file);
149        if (name == null) {
150            log.error(
151                    String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", file.getAbsolutePath()));
152            return null;
153        }
154
155        String path = file.getAbsolutePath();
156
157        log.info(String.format("Before deploy bundle for file at '%s'\n" + "%s", path, getRuntimeStatus()));
158
159        if (reloadResourceClasspath) {
160            URL url;
161            try {
162                url = new File(path).toURI().toURL();
163            } catch (MalformedURLException e) {
164                throw new RuntimeException(e);
165            }
166            Framework.reloadResourceLoader(Arrays.asList(url), null);
167        }
168
169        // check if this is a bundle first
170        Bundle newBundle = getBundleContext().installBundle(path);
171        if (newBundle == null) {
172            throw new IllegalArgumentException("Could not find a valid bundle at path: " + path);
173        }
174        Transaction tx = TransactionHelper.suspendTransaction();
175        try {
176            newBundle.start();
177        } finally {
178            TransactionHelper.resumeTransaction(tx);
179        }
180
181        log.info(String.format("Deploy done for bundle with name '%s'.\n" + "%s", newBundle.getSymbolicName(),
182                getRuntimeStatus()));
183
184        return newBundle.getSymbolicName();
185    }
186
187    @Override
188    public void undeployBundle(File file, boolean reloadResources) throws BundleException {
189        String name = getOSGIBundleName(file);
190        String path = file.getAbsolutePath();
191        if (name == null) {
192            log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", path));
193            return;
194        }
195
196        undeployBundle(name);
197
198        if (reloadResources) {
199            URL url;
200            try {
201                url = new File(path).toURI().toURL();
202            } catch (MalformedURLException e) {
203                throw new RuntimeException(e);
204            }
205            Framework.reloadResourceLoader(null, Arrays.asList(url));
206        }
207    }
208
209    @Override
210    public void undeployBundle(String bundleName) throws BundleException {
211        if (bundleName == null) {
212            // ignore
213            return;
214        }
215        log.info(String.format("Before undeploy bundle with name '%s'.\n" + "%s", bundleName, getRuntimeStatus()));
216        BundleContext ctx = getBundleContext();
217        ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName());
218        PackageAdmin srv = (PackageAdmin) ctx.getService(ref);
219        try {
220            for (Bundle b : srv.getBundles(bundleName, null)) {
221                if (b != null && b.getState() == Bundle.ACTIVE) {
222                    Transaction tx = TransactionHelper.suspendTransaction();
223                    try {
224                        b.stop();
225                        b.uninstall();
226                    } finally {
227                        TransactionHelper.resumeTransaction(tx);
228                    }
229                }
230            }
231        } finally {
232            ctx.ungetService(ref);
233        }
234        log.info(String.format("Undeploy done.\n" + "%s", getRuntimeStatus()));
235    }
236
237    @Override
238    public Long lastFlushed() {
239        return lastFlushed;
240    }
241
242    /**
243     * Sets the last date date to current date timestamp
244     *
245     * @since 5.6
246     */
247    protected void setFlushedNow() {
248        lastFlushed = Long.valueOf(System.currentTimeMillis());
249    }
250
251    /**
252     * @deprecated since 5.6, use {@link #runDeploymentPreprocessor()} instead
253     */
254    @Override
255    @Deprecated
256    public void installWebResources(File file) throws IOException {
257        log.info("Install web resources");
258        if (file.isDirectory()) {
259            File war = new File(file, "web");
260            war = new File(war, "nuxeo.war");
261            if (war.isDirectory()) {
262                FileUtils.copyTree(war, getAppDir());
263            } else {
264                // compatibility mode with studio 1.5 - see NXP-6186
265                war = new File(file, "nuxeo.war");
266                if (war.isDirectory()) {
267                    FileUtils.copyTree(war, getAppDir());
268                }
269            }
270        } else if (file.isFile()) { // a jar
271            File war = getWarDir();
272            ZipUtils.unzip("web/nuxeo.war", file, war);
273            // compatibility mode with studio 1.5 - see NXP-6186
274            ZipUtils.unzip("nuxeo.war", file, war);
275        }
276    }
277
278    @Override
279    public void runDeploymentPreprocessor() throws IOException {
280        if (log.isDebugEnabled()) {
281            log.debug("Start running deployment preprocessor");
282        }
283        String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath();
284        File root = new File(rootPath);
285        DeploymentPreprocessor processor = new DeploymentPreprocessor(root);
286        // initialize
287        processor.init();
288        // and predeploy
289        processor.predeploy();
290        if (log.isDebugEnabled()) {
291            log.debug("Deployment preprocessing done");
292        }
293    }
294
295    protected static File getAppDir() {
296        return Environment.getDefault().getConfig().getParentFile();
297    }
298
299    protected static File getWarDir() {
300        return new File(getAppDir(), "nuxeo.war");
301    }
302
303    @Override
304    public String getOSGIBundleName(File file) {
305        Manifest mf = JarUtils.getManifest(file);
306        if (mf == null) {
307            return null;
308        }
309        String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName");
310        if (bundleName == null) {
311            return null;
312        }
313        int index = bundleName.indexOf(';');
314        if (index > -1) {
315            bundleName = bundleName.substring(0, index);
316        }
317        return bundleName;
318    }
319
320    protected String getRuntimeStatus() {
321        StringBuilder msg = new StringBuilder();
322        RuntimeService runtime = Framework.getRuntime();
323        runtime.getStatusMessage(msg);
324        return msg.toString();
325    }
326
327
328    protected void triggerReloadWithNewTransaction(String id) {
329        if (TransactionHelper.isTransactionMarkedRollback()) {
330            throw new AssertionError("The calling transaction is marked rollback");
331        }
332        Transaction tx = TransactionHelper.suspendTransaction();
333        TransactionHelper.startTransaction();
334        try {
335            try {
336                triggerReloadWithPassivate(id);
337            } catch (RuntimeException cause) {
338                TransactionHelper.setTransactionRollbackOnly();
339                throw cause;
340            }
341        } finally {
342            boolean wasRollbacked = TransactionHelper.isTransactionMarkedRollback();
343            TransactionHelper.commitOrRollbackTransaction();
344            TransactionHelper.resumeTransaction(tx);
345            if (TransactionHelper.isTransactionActive()) {
346                if (wasRollbacked) {
347                    TransactionHelper.setTransactionRollbackOnly();
348                }
349                TransactionHelper.commitOrRollbackTransaction(); // should flush
350                TransactionHelper.startTransaction();
351            }
352        }
353    }
354
355    protected void triggerReloadWithPassivate(String id) {
356        log.info("about to passivate for " + id);
357        Framework.getLocalService(EventService.class)
358                .sendEvent(new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null));
359        try {
360            ServicePassivator.proceed(Duration.ofSeconds(5), Duration.ofSeconds(30), true, () -> {
361                log.info("about to send " + id);
362                Framework.getLocalService(EventService.class)
363                        .sendEvent(new Event(RELOAD_TOPIC, id, this, null));
364            })
365                    .onFailure(snapshot -> {
366                        throw new UnsupportedOperationException(
367                                "Detected access, should initiate a reboot " + snapshot.toString());
368                    });
369        } finally {
370            Framework.getLocalService(EventService.class)
371                    .sendEvent(new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null));
372            log.info("returning from " + id);
373        }
374    }
375}