com.gitpitch.services.OfflineService.java Source code

Java tutorial

Introduction

Here is the source code for com.gitpitch.services.OfflineService.java

Source

/*
 * MIT License
 *
 * Copyright (c) 2016 David Russell
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.gitpitch.services;

import com.gitpitch.factory.MarkdownModelFactory;
import com.gitpitch.git.*;
import com.gitpitch.models.MarkdownModel;
import com.gitpitch.models.SlideshowModel;
import com.gitpitch.executors.BackEndThreads;
import com.gitpitch.utils.PitchParams;
import com.gitpitch.utils.YAMLOptions;
import com.gitpitch.utils.MarkdownRenderer;
import org.apache.commons.io.FilenameUtils;
import play.Configuration;
import play.Environment;
import play.Logger;

import javax.inject.*;
import java.io.File;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.FileSystems;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/*
 * PITCHME.zip archive offline service.
 */
@Singleton
public class OfflineService {

    private final Logger.ALogger log = Logger.of(this.getClass());

    private final GRSManager grsManager;
    private final DiskService diskService;
    private final ShellService shellService;
    private final MarkdownModelFactory markdownModelFactory;
    private final BackEndThreads backEndThreads;
    private final Configuration configuration;
    private final Environment env;

    @Inject
    public OfflineService(GRSManager grsManager, DiskService diskService, ShellService shellService,
            MarkdownModelFactory markdownModelFactory, BackEndThreads backEndThreads, Configuration configuration,
            Environment env) {

        this.grsManager = grsManager;
        this.diskService = diskService;
        this.shellService = shellService;
        this.markdownModelFactory = markdownModelFactory;
        this.backEndThreads = backEndThreads;
        this.configuration = configuration;
        this.env = env;
    }

    /*
     * Generate and fetch PITCHME.zip.
     */
    public CountDownLatch fetchZip(PitchParams pp, Optional<SlideshowModel> ssmo) {

        log.debug("fetchZip: pp={}, ssmo.isPresent={}", pp, ssmo.isPresent());

        final String zipKey = MarkdownModel.genKey(pp);

        CountDownLatch freshLatch = new CountDownLatch(1);
        CountDownLatch activeLatch = zipLatchMap.putIfAbsent(zipKey, freshLatch);

        if (activeLatch != null) {
            /*
             * A non-null activeLatch implies a fetchZip()
             * operation is already in progress for the current
             * /{user}/{repo}?b={branch}.
             */
            log.debug("fetchZip: pp={}, already in progress, " + "returning existing activeLatch={}", pp,
                    activeLatch);
            return activeLatch;

        } else {

            CompletableFuture<Void> syncFuture = CompletableFuture.supplyAsync(() -> {

                Path branchPath = diskService.ensure(pp);
                int zipStatus = generateZip(pp, ssmo);

                if (zipStatus != STATUS_OK) {
                    log.warn("fetchZip: pp={}, fetch status={}", pp, zipStatus);
                }

                /*
                 * Current operation completed, so remove latch associated
                 * with operation from zipLatchMap to permit future
                 * operations on this /{user}/{repo}?b={branch}.
                 */
                releaseCountDownLatch(zipLatchMap, zipKey);

                /*
                 * Operation completed, valid result cached, no return required.
                 */
                return null;

            }, backEndThreads.POOL).handle((result, error) -> {

                if (error != null) {

                    log.warn("fetchZip: pp={}, fetch error={}", pp, error);
                    releaseCountDownLatch(zipLatchMap, zipKey);
                }

                return null;
            });

            return freshLatch;
        }

    }

    /*
     * Call when fetchXXX operation has completed either
     * successfully or on error. Remove latch associated with
     * operation from xxxLatchMap to permit future operations
     * on /{user}/{repo}?b={branch} associated with latchKey.
     */
    private void releaseCountDownLatch(ConcurrentHashMap<String, CountDownLatch> latchMap, String latchKey) {

        CountDownLatch completedLatch = latchMap.remove(latchKey);

        if (completedLatch != null) {
            completedLatch.countDown();
        }

    }

    /*
     * Generate offline slideshow as self-contained zip archive.
     */
    private int generateZip(PitchParams pp, Optional<SlideshowModel> ssmo) {

        log.debug("generateZip: pp={}, ssmo.isPresent={}", pp, ssmo.isPresent());

        int status = STATUS_UNDEF;

        Path zipRoot = null;

        try {

            zipRoot = prepareZipRoot(pp);

            status = fetchOnlineMarkdown(pp, zipRoot);

            if (status == STATUS_OK)
                status = processMarkdown(pp, zipRoot, ssmo);

            if (status == STATUS_OK)
                status = fetchLandingHTML(pp, zipRoot);

            if (status == STATUS_OK)
                status = fetchSlideshowHTML(pp, zipRoot);

            if (status == STATUS_OK)
                fetchFixedDependencies(pp, zipRoot);

            if (status == STATUS_OK)
                fetchYAMLDependencies(pp, zipRoot);

            if (status == STATUS_OK)
                status = buildZip(pp, zipRoot);

        } catch (Exception zex) {
            log.warn("generateZip: pp={}, ex={}", zex);
        } finally {

            /*
             * Clean up artifacts if failed to generate zip.
             */
            if (status != STATUS_OK && zipRoot != null) {
                diskService.delete(zipRoot.resolve(PITCHME_ZIP));
                diskService.deepDelete(zipRoot.resolve(ZIP_ROOT_DIR).toFile());
            }
        }

        return status;
    }

    /*
     * Prepare zip archive root directory on disk.
     */
    private Path prepareZipRoot(PitchParams pp) {
        Path zipRoot = diskService.ensure(pp);
        if (pp.pitchme != null) {
            zipRoot = zipRoot.resolve(pp.pitchme);
        }
        zipRoot = zipRoot.resolve(ZIP_ROOT_DIR);
        return diskService.ensure(zipRoot);
    }

    /*
     * Fetch online PITCHME.md into ZIP_MD_DIR.
     */
    private int fetchOnlineMarkdown(PitchParams pp, Path zipRoot) {

        String murl = com.gitpitch.controllers.routes.PitchController
                .markdown(pp.grs, pp.user, pp.repo, pp.branch, pp.pitchme).absoluteURL(isEncrypted(), hostname());

        Path zipMdPath = diskService.ensure(zipRoot.resolve(ZIP_MD_DIR));
        return diskService.download(pp, zipMdPath, murl, PITCHME_ONLINE_MD, grsManager.get(pp).getHeaders());
    }

    /*
     * Fetch Landing.html.
     */
    private int fetchLandingHTML(PitchParams pp, Path zipRoot) {

        String lurl = com.gitpitch.controllers.routes.PitchController
                .landing(pp.user, pp.repo, pp.branch, pp.grs, pp.theme, pp.pitchme, pp.notes, ENABLED)
                .absoluteURL(isEncrypted(), hostname());

        return diskService.download(pp, zipRoot, lurl, INDEX_HTML, grsManager.get(pp).getHeaders());
    }

    /*
     * Fetch Slideshow.html.
     */
    private int fetchSlideshowHTML(PitchParams pp, Path zipRoot) {

        String surl = com.gitpitch.controllers.routes.PitchController.slideshow(pp.grs, pp.user, pp.repo, pp.branch,
                pp.theme, pp.pitchme, pp.notes, ENABLED, ENABLED, null).absoluteURL(isEncrypted(), hostname());

        return diskService.download(pp, zipRoot, surl, SLIDESHOW_HTML, grsManager.get(pp).getHeaders());
    }

    /*
     * Fetch fixed JS and CSS dependencies for GitPitch slideshow.
     */
    private void fetchFixedDependencies(PitchParams pp, Path zipRoot) throws java.io.IOException {

        Path destPath = diskService.ensure(zipRoot.resolve(ZIP_ASSETS_DIR));

        if (env.isProd()) {

            Path jarPath = prodModeDependenciesJar();
            if (jarPath.toFile().exists()) {
                diskService.copyDirectoryFromJar(jarPath, FIXED_ASSETS, destPath);
            } else {
                log.warn("fetchFixedDependencies: [ prod ] jar " + "not found={}", jarPath);
            }

        } else {

            Path fixedAssetsPath = devModeFixedDependencies();
            if (fixedAssetsPath.toFile().exists()) {
                diskService.copyDirectory(fixedAssetsPath, destPath);
            } else {
                log.debug("fetchFixedDependencies: [ dev ] fixed assets " + "not found={}", fixedAssetsPath);
            }
        }
    }

    /*
     * Convert online PITCHME.md to offline PITCHME.md.
     */
    private int processMarkdown(PitchParams pp, Path zipRoot, Optional<SlideshowModel> ssmo) {

        int status = STATUS_UNDEF;

        String consumed = null;
        Path mdOnlinePath = zipRoot.resolve(PITCHME_ONLINE_PATH);
        File mdOnlineFile = mdOnlinePath.toFile();

        if (mdOnlineFile.exists()) {

            GRSService grsService = grsManager.getService(grsManager.get(pp));

            MarkdownRenderer mrndr = MarkdownRenderer.build(pp, ssmo, grsService, diskService);

            MarkdownModel markdownModel = (MarkdownModel) markdownModelFactory.create(mrndr);

            try (Stream<String> stream = Files.lines(mdOnlinePath)) {

                consumed = stream.map(md -> {
                    return markdownModel.offline(md);
                }).collect(Collectors.joining("\n"));

                Path mdOfflinePath = zipRoot.resolve(PITCHME_OFFLINE_PATH);
                Files.write(mdOfflinePath, consumed.getBytes());

                fetchOnlineAssets(pp, zipRoot);

                status = STATUS_OK;

            } catch (Exception mex) {
                log.warn("processMarkdown: ex={}", mex);
            }

        } else {
            log.warn("processMarkdown: mdOnline not found={}", mdOnlineFile);
        }

        log.debug("processMarkdown: returning status={}", status);
        return status;
    }

    /*
     * Fetch online PITCHME.md image assets into ZIP_MD_ASSETS_DIR.
     */
    private void fetchOnlineAssets(PitchParams pp, Path zipRoot) {

        List<String> assetUrls = null;

        Path mdOnlinePath = zipRoot.resolve(PITCHME_ONLINE_PATH);
        File mdOnlineFile = mdOnlinePath.toFile();

        if (mdOnlineFile.exists()) {

            MarkdownModel markdownModel = (MarkdownModel) markdownModelFactory.create(null);

            try (Stream<String> stream = Files.lines(mdOnlinePath)) {

                assetUrls = stream.map(md -> {
                    return markdownModel.offlineAssets(md);
                }).collect(Collectors.toList());

                log.debug("fetchOnlineAssets: assetUrls={}", assetUrls);

                Path zipMdAssetsPath = zipRoot.resolve(ZIP_MD_ASSETS_DIR);
                zipMdAssetsPath = diskService.ensure(zipMdAssetsPath);

                List<String> fetched = new ArrayList<String>();

                for (String assetUrl : assetUrls) {
                    if (assetUrl != null && !fetched.contains(assetUrl)) {
                        diskService.download(pp, zipMdAssetsPath, assetUrl, FilenameUtils.getName(assetUrl),
                                grsManager.get(pp).getHeaders());
                        fetched.add(assetUrl);
                    }
                }

            } catch (Exception mex) {
                log.warn("fetchOnlineAssets: ex={}", mex);
            }

        } else {
            log.warn("fetchOnlineAssets: mdOnline not found={}", mdOnlineFile);
        }
    }

    /*
     * Fetch PITCHME.yaml dependencies into zip archive.
     */
    private void fetchYAMLDependencies(PitchParams pp, Path zipRoot) {

        GRSService grsService = grsManager.getService(grsManager.get(pp));
        YAMLOptions yOpts = YAMLOptions.build(pp, grsService, diskService);
        log.debug("fetchYAMLDependencies: yOpts={}", yOpts);

        try {

            if (yOpts != null && yOpts.hasLogo()) {
                String logoUrl = yOpts.fetchLogo(pp);
                String logoName = FilenameUtils.getName(logoUrl);

                Path zipAssetsPath = diskService.ensure(zipRoot.resolve(ZIP_ASSETS_DIR));
                diskService.download(pp, zipAssetsPath, logoUrl, logoName, grsManager.get(pp).getHeaders());
                log.debug("fetchYAMLDependencies: downloaded logo={}", logoUrl);
            }

        } catch (Exception lex) {
            log.warn("fetchYAMLDependencies: logo ex={}", lex);
        }

        try {

            /*
             * If Math Slides not enabled within PITCHME.yaml, strip
             * Reveal.js math plugin file dependencies from zip.
             */
            if (yOpts == null || !yOpts.mathEnabled(pp)) {

                Path destPath = zipRoot.resolve(ZIP_ASSETS_DIR);
                String revealVersion = configuration.getString("gitpitch.dependency.revealjs");
                Path mathPluginPath = Paths.get(destPath.toString(), "reveal.js", revealVersion, "plugin/math");
                log.debug("fetchYAMLDependencies: removing mathPlugin={}", mathPluginPath);

                diskService.deepDelete(mathPluginPath.toFile());
            }

        } catch (Exception mex) {
            log.warn("fetchYAMLDependencies: math config assets ex={}", mex);
        }
    }

    /*
     * Build PITCHME.zip.
     */
    private int buildZip(PitchParams pp, Path zipRoot) {

        /*
         * Remove PITCHME_ONLINE_MD from zip directory.
         */
        Path onlinePath = zipRoot.resolve(PITCHME_ONLINE_PATH);
        diskService.delete(onlinePath);

        /*
         * CMD: zip -r PITCHME.zip PITCHME
         */
        String[] cmd = { ZIP_CMD, ZIP_QUIET, ZIP_ALL, PITCHME_ZIP, ZIP_ROOT_DIR };

        Path zipWd = diskService.ensure(pp, pp.pitchme);
        return shellService.exec(ZIP_CMD, pp, zipWd, cmd);
    }

    public boolean isEncrypted() {
        return configuration.getBoolean("gitpitch.https");
    }

    public String hostname() {
        return configuration.getString("gitpitch.hostname");
    }

    public Path devModeFixedDependencies() {
        String fixedAssets = configuration.getString("gitpitch.offline.dev.fixed.assets.home");
        return Paths.get(fixedAssets);
    }

    public Path prodModeDependenciesJar() {
        String jarAssets = configuration.getString("gitpitch.offline.prod.fixed.assets.home");
        return Paths.get(jarAssets);
    }

    private static final String ZIP_ROOT_DIR = "PITCHME";
    private static final String ZIP_MD_DIR = "assets/md";
    private static final String ZIP_MD_ASSETS_DIR = "assets/md/assets";
    private static final String ZIP_ASSETS_DIR = "assets";
    private static final String PITCHME_ONLINE_MD = "PITCHME.mdo";
    private static final String PITCHME_OFFLINE_MD = "PITCHME.md";
    private static final String PITCHME_ONLINE_PATH = ZIP_MD_DIR + "/" + PITCHME_ONLINE_MD;
    private static final String PITCHME_OFFLINE_PATH = ZIP_MD_DIR + "/" + PITCHME_OFFLINE_MD;
    private static final String PITCHME_ZIP = "PITCHME.zip";
    private static final String PITCHME_CSS = "PITCHME.css";
    private static final String INDEX_HTML = "index.html";
    private static final String SLIDESHOW_HTML = "pitchme.html";
    private static final String FIXED_ASSETS = "/public/libs";
    private static final String FIXED_ASSETS_MATH = "/public/libs-math";
    private static final String ZIP_CMD = "zip";
    private static final String ZIP_QUIET = "-q";
    private static final String ZIP_ALL = "-r";
    private static final String ENABLED = "true";
    private static final int STATUS_OK = 0;
    private static final int STATUS_UNDEF = 999;
    private final ConcurrentHashMap<String, CountDownLatch> zipLatchMap = new ConcurrentHashMap();
}