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