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 *     Kevin Leturc <kleturc@nuxeo.com>
019 */
020package org.nuxeo.runtime.tomcat.dev;
021
022import java.io.BufferedWriter;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.lang.management.ManagementFactory;
027import java.lang.reflect.Method;
028import java.net.URI;
029import java.net.URL;
030import java.nio.file.CopyOption;
031import java.nio.file.FileSystem;
032import java.nio.file.FileSystems;
033import java.nio.file.FileVisitResult;
034import java.nio.file.Files;
035import java.nio.file.Path;
036import java.nio.file.SimpleFileVisitor;
037import java.nio.file.attribute.BasicFileAttributes;
038import java.util.ArrayList;
039import java.util.Collections;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.Timer;
044import java.util.TimerTask;
045
046import javax.management.JMException;
047import javax.management.MBeanServer;
048import javax.management.ObjectName;
049
050import org.apache.commons.logging.Log;
051import org.apache.commons.logging.LogFactory;
052import org.nuxeo.osgi.application.FrameworkBootstrap;
053import org.nuxeo.osgi.application.MutableClassLoader;
054
055/**
056 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
057 */
058public class DevFrameworkBootstrap extends FrameworkBootstrap implements DevBundlesManager {
059
060    public static final String DEV_BUNDLES_NAME = "org.nuxeo:type=sdk,name=dev-bundles";
061
062    public static final String WEB_RESOURCES_NAME = "org.nuxeo:type=sdk,name=web-resources";
063
064    public static final String USE_COMPAT_HOT_RELOAD = "nuxeo.hotreload.compat.mechanism";
065
066    protected static final String DEV_BUNDLES_CP = "dev-bundles/*";
067
068    protected final Log log = LogFactory.getLog(DevFrameworkBootstrap.class);
069
070    protected DevBundle[] devBundles;
071
072    protected Timer bundlesCheck;
073
074    protected long lastModified = 0;
075
076    protected ReloadServiceInvoker reloadServiceInvoker;
077
078    protected File devBundlesFile;
079
080    protected final File seamdev;
081
082    protected final File webclasses;
083
084    protected boolean compatHotReload;
085
086    public DevFrameworkBootstrap(MutableClassLoader cl, File home) throws IOException {
087        super(cl, home);
088        devBundlesFile = new File(home, "dev.bundles");
089        seamdev = new File(home, "nuxeo.war/WEB-INF/dev");
090        webclasses = new File(home, "nuxeo.war/WEB-INF/classes");
091        devBundles = new DevBundle[0];
092    }
093
094    @Override
095    public void start(MutableClassLoader cl) throws ReflectiveOperationException, IOException, JMException {
096        // check if we have dev. bundles or libs to deploy and add them to the
097        // classpath
098        preloadDevBundles();
099        // start the framework
100        super.start(cl);
101        ClassLoader loader = (ClassLoader) this.loader;
102        reloadServiceInvoker = new ReloadServiceInvoker(loader);
103        compatHotReload = new FrameworkInvoker(loader).isBooleanPropertyTrue(USE_COMPAT_HOT_RELOAD);
104        writeComponentIndex();
105        postloadDevBundles(); // start dev bundles if any
106        String installReloadTimerOption = (String) env.get(INSTALL_RELOAD_TIMER);
107        if (installReloadTimerOption != null && Boolean.parseBoolean(installReloadTimerOption)) {
108            toggleTimer();
109        }
110        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
111        server.registerMBean(this, new ObjectName(DEV_BUNDLES_NAME));
112        server.registerMBean(cl, new ObjectName(WEB_RESOURCES_NAME));
113    }
114
115    @Override
116    protected void initializeEnvironment() throws IOException {
117        super.initializeEnvironment();
118        // add the dev-bundles to classpath
119        env.computeIfPresent(BUNDLES, (k, v) -> v + ":" + DEV_BUNDLES_CP);
120    }
121
122    @Override
123    public void toggleTimer() {
124        // start reload timer
125        if (isTimerRunning()) {
126            bundlesCheck.cancel();
127            bundlesCheck = null;
128        } else {
129            bundlesCheck = new Timer("Dev Bundles Loader");
130            bundlesCheck.scheduleAtFixedRate(new TimerTask() {
131                @Override
132                public void run() {
133                    try {
134                        loadDevBundles();
135                    } catch (RuntimeException e) {
136                        log.error("Failed to reload dev bundles", e);
137                    }
138                }
139            }, 2000, 2000);
140        }
141    }
142
143    @Override
144    public boolean isTimerRunning() {
145        return bundlesCheck != null;
146    }
147
148    @Override
149    public void stop(MutableClassLoader cl) throws ReflectiveOperationException, JMException {
150        if (bundlesCheck != null) {
151            bundlesCheck.cancel();
152            bundlesCheck = null;
153        }
154        try {
155            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
156            server.unregisterMBean(new ObjectName(DEV_BUNDLES_NAME));
157            server.unregisterMBean(new ObjectName(WEB_RESOURCES_NAME));
158        } finally {
159            super.stop(cl);
160        }
161    }
162
163    @Override
164    public String getDevBundlesLocation() {
165        return devBundlesFile.getAbsolutePath();
166    }
167
168    /**
169     * Load the development bundles and libs if any in the classpath before starting the framework.
170     *
171     * @deprecated since 9.3, we now have a new mechanism to hot reload bundles from {@link #devBundlesFile}. The new
172     *             mechanism copies bundles to nxserver/bundles, so it's now useless to preload dev bundles as they're
173     *             deployed as a regular bundle.
174     */
175    @Deprecated
176    protected void preloadDevBundles() throws IOException {
177        if (!compatHotReload) {
178            return;
179        }
180        if (!devBundlesFile.isFile()) {
181            return;
182        }
183        lastModified = devBundlesFile.lastModified();
184        devBundles = DevBundle.parseDevBundleLines(new FileInputStream(devBundlesFile));
185        if (devBundles.length > 0) {
186            installNewClassLoader(devBundles);
187        }
188    }
189
190    /**
191     * @deprecated since 9.3, we now have a new mechanism to hot reload bundles from {@link #devBundlesFile}. The new
192     *             mechanism copies bundles to nxserver/bundles, so it's now useless to postload dev bundles as they're
193     *             deployed as a regular bundle.
194     */
195    @Deprecated
196    protected void postloadDevBundles() throws ReflectiveOperationException {
197        if (!compatHotReload) {
198            return;
199        }
200        if (devBundles.length > 0) {
201            reloadServiceInvoker.hotDeployBundles(devBundles);
202        }
203    }
204
205    @Override
206    public void loadDevBundles() {
207        long tm = devBundlesFile.lastModified();
208        if (lastModified >= tm) {
209            return;
210        }
211        lastModified = tm;
212        try {
213            reloadDevBundles(DevBundle.parseDevBundleLines(new FileInputStream(devBundlesFile)));
214        } catch (ReflectiveOperationException | IOException e) {
215            throw new RuntimeException("Failed to reload dev bundles", e);
216        }
217    }
218
219    @Override
220    public void resetDevBundles(String path) {
221        try {
222            devBundlesFile = new File(path);
223            lastModified = 0;
224            loadDevBundles();
225        } catch (RuntimeException e) {
226            log.error("Unable to reset dev bundles", e);
227        }
228    }
229
230    @Override
231    public DevBundle[] getDevBundles() {
232        return devBundles;
233    }
234
235    protected synchronized void reloadDevBundles(DevBundle[] bundles) throws ReflectiveOperationException, IOException {
236        long begin = System.currentTimeMillis();
237
238        if (compatHotReload) {
239            if (devBundles.length > 0) { // clear last context
240                try {
241                    reloadServiceInvoker.hotUndeployBundles(devBundles);
242                    clearClassLoader();
243                } finally {
244                    devBundles = new DevBundle[0];
245                }
246            }
247
248            if (bundles.length > 0) { // create new context
249                try {
250                    installNewClassLoader(bundles);
251                    reloadServiceInvoker.hotDeployBundles(bundles);
252                } finally {
253                    devBundles = bundles;
254                }
255            }
256        } else {
257            // symbolicName of bundlesToDeploy will be filled by hotReloadBundles before hot reload
258            // -> this allows server to be hot reloaded again in case of errors
259            // if everything goes fine, bundlesToDeploy will be replaced by result of hot reload containing symbolic
260            // name and the new bundle path
261            DevBundle[] bundlesToDeploy = bundles;
262            try {
263                bundlesToDeploy = reloadServiceInvoker.hotReloadBundles(devBundles, bundlesToDeploy);
264
265                // write the new dev bundles location to the file
266                writeDevBundles(bundlesToDeploy);
267            } finally {
268                devBundles = bundlesToDeploy;
269            }
270        }
271        if (log.isInfoEnabled()) {
272            log.info(String.format("Hot reload has been run in %s ms", System.currentTimeMillis() - begin));
273        }
274    }
275
276    /**
277     * Writes to the {@link #devBundlesFile} the input {@code devBundles} by replacing the former file.
278     * <p />
279     * This method will {@link #toggleTimer() toggle} the file update check timer if needed.
280     *
281     * @since 9.3
282     */
283    protected void writeDevBundles(DevBundle[] devBundles) throws IOException {
284        boolean timerExists = isTimerRunning();
285        if (timerExists) {
286            // timer is running, we need to stop it before editing the file
287            toggleTimer();
288        }
289        // newBufferedWriter without OpenOption will create/truncate if exist the target file
290        try (BufferedWriter writer = Files.newBufferedWriter(devBundlesFile.toPath())) {
291            for (DevBundle devBundle : devBundles) {
292                writer.write(devBundle.toString());
293            }
294        } finally {
295            if (timerExists) {
296                // restore the time status
297                lastModified = System.currentTimeMillis();
298                toggleTimer();
299            }
300        }
301    }
302
303    /**
304     * Zips recursively the content of {@code source} to the {@code target} zip file.
305     *
306     * @since 9.3
307     */
308    protected Path zipDirectory(Path source, Path target, CopyOption... options) throws IOException {
309        if (!source.toFile().isDirectory()) {
310            throw new IllegalArgumentException("Source argument must be a directory to zip");
311        }
312        // locate file system by using the syntax defined in java.net.JarURLConnection
313        URI uri = URI.create("jar:file:" + target.toString());
314
315        try (FileSystem zipfs = FileSystems.newFileSystem(uri, Collections.singletonMap("create", "true"))) {
316            Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
317
318                @Override
319                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
320                    if (source.equals(dir)) {
321                        // don't process root element
322                        return FileVisitResult.CONTINUE;
323                    }
324                    return visitFile(dir, attrs);
325                }
326
327                @Override
328                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
329                    // retrieve the destination path in zip
330                    Path relativePath = source.relativize(file);
331                    Path pathInZipFile = zipfs.getPath(relativePath.toString());
332                    // copy a file into the zip file
333                    Files.copy(file, pathInZipFile, options);
334                    return FileVisitResult.CONTINUE;
335                }
336
337            });
338        }
339        return target;
340    }
341
342    /**
343     * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload}
344     */
345    @Deprecated
346    protected void clearClassLoader() {
347        NuxeoDevWebappClassLoader devLoader = (NuxeoDevWebappClassLoader) loader;
348        devLoader.clear();
349        System.gc();
350    }
351
352    /**
353     * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload}
354     */
355    @Deprecated
356    protected void installNewClassLoader(DevBundle[] bundles) {
357        List<URL> jarUrls = new ArrayList<>();
358        List<File> seamDirs = new ArrayList<>();
359        List<File> resourceBundleFragments = new ArrayList<>();
360        // filter dev bundles types
361        for (DevBundle bundle : bundles) {
362            if (bundle.devBundleType.isJar) {
363                try {
364                    jarUrls.add(bundle.url());
365                } catch (IOException e) {
366                    log.error("Cannot install " + bundle);
367                }
368            } else if (bundle.devBundleType == DevBundleType.Seam) {
369                seamDirs.add(bundle.file());
370            } else if (bundle.devBundleType == DevBundleType.ResourceBundleFragment) {
371                resourceBundleFragments.add(bundle.file());
372            }
373        }
374
375        // install class loader
376        NuxeoDevWebappClassLoader devLoader = (NuxeoDevWebappClassLoader) loader;
377        devLoader.createLocalClassLoader(jarUrls.toArray(new URL[jarUrls.size()]));
378
379        // install seam classes in hot sync folder
380        try {
381            installSeamClasses(seamDirs.toArray(new File[seamDirs.size()]));
382        } catch (IOException e) {
383            log.error("Cannot install seam classes in hotsync folder", e);
384        }
385
386        // install l10n resources
387        try {
388            installResourceBundleFragments(resourceBundleFragments);
389        } catch (IOException e) {
390            log.error("Cannot install l10n resources", e);
391        }
392    }
393
394    public void writeComponentIndex() {
395        File file = new File(home.getParentFile(), "sdk");
396        file.mkdirs();
397        file = new File(file, "components.index");
398        try {
399            Method m = getClassLoader().loadClass("org.nuxeo.runtime.model.impl.ComponentRegistrySerializer")
400                    .getMethod("writeIndex", File.class);
401            m.invoke(null, file);
402        } catch (ReflectiveOperationException t) {
403            // ignore
404        }
405    }
406
407    /**
408     * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload}
409     */
410    @Deprecated
411    public void installSeamClasses(File[] dirs) throws IOException {
412        if (seamdev.exists()) {
413            IOUtils.deleteTree(seamdev);
414        }
415        seamdev.mkdirs();
416        for (File dir : dirs) {
417            IOUtils.copyTree(dir, seamdev);
418        }
419    }
420
421    /**
422     * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload}
423     */
424    @Deprecated
425    public void installResourceBundleFragments(List<File> files) throws IOException {
426        Map<String, List<File>> fragments = new HashMap<>();
427
428        for (File file : files) {
429            String name = resourceBundleName(file);
430            if (!fragments.containsKey(name)) {
431                fragments.put(name, new ArrayList<>());
432            }
433            fragments.get(name).add(file);
434        }
435        for (String name : fragments.keySet()) {
436            IOUtils.appendResourceBundleFragments(name, fragments.get(name), webclasses);
437        }
438    }
439
440    /**
441     * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload}
442     */
443    @Deprecated
444    protected static String resourceBundleName(File file) {
445        String name = file.getName();
446        return name.substring(name.lastIndexOf('-') + 1);
447    }
448
449}