001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.assets;
019
020import static io.stallion.utils.Literals.*;
021import static io.stallion.Context.*;
022
023import java.io.File;
024import java.io.IOException;
025import java.io.UnsupportedEncodingException;
026import java.net.URISyntaxException;
027import java.net.URL;
028import java.net.URLEncoder;
029import java.text.MessageFormat;
030import java.util.List;
031import java.util.Map;
032
033import io.stallion.Context;
034import io.stallion.assetBundling.*;
035import io.stallion.exceptions.UsageException;
036import io.stallion.plugins.PluginRegistry;
037import io.stallion.services.Log;
038import io.stallion.settings.Settings;
039import org.apache.commons.codec.digest.DigestUtils;
040import org.apache.commons.io.IOUtils;
041import org.apache.http.client.utils.URLEncodedUtils;
042
043
044public class ResourceAssetBundleRenderer {
045
046    private String path;
047    private String plugin;
048    private String extension;
049    private Class resourceClass;
050    private URL resourceUrl;
051
052    public ResourceAssetBundleRenderer(String plugin, String path) {
053        this.plugin = or(plugin, "stallion");
054        if (path.endsWith(".bundle")) {
055            throw new UsageException("Invalid bundle path: " + path + ". The path must end with the extension for the type of content from the bundle that you want to render - .css, .js, or .head.js");
056        }
057        path = AssetsController.ensureSafeAssetsPath(path);
058        int i = path.indexOf(".bundle.") + 7;
059        if (i == -1) {
060            throw new UsageException("The path - "  + path + " - is not a valid bundle name. Expected to end with .bundle.js, .bundle.head.js, or .bundle.css");
061        }
062        this.extension = path.substring(i);
063        this.path = path.substring(0, i);
064        this.resourceClass = ResourceAssetBundleRenderer.class;
065        if (!empty(plugin) && !plugin.equals("stallion")) {
066            this.resourceClass = PluginRegistry.instance().getJavaPluginByName().get(plugin).getClass();
067        }
068        resourceUrl = resourceClass.getResource(this.path);
069        if (resourceUrl == null) {
070            throw new UsageException("The bundle you are trying to render - " + this.path + " - does not exist in the plugin with class " + resourceClass.getCanonicalName());
071        }
072
073    }
074
075    public String renderDebugHtml() {
076        if (!resourceUrl.getProtocol().equals("file")) {
077            return renderProductionHtml();
078        }
079        String bundleFilePath = null;
080        try {
081            bundleFilePath = resourceUrl.toURI().getPath();
082        } catch (URISyntaxException e) {
083            throw new RuntimeException(e);
084        }
085        File debugVersion = new File(bundleFilePath.replace("/target/classes/", "/src/main/resources/"));
086        if (debugVersion.exists()) {
087            bundleFilePath = debugVersion.getAbsolutePath();
088        }
089        AssetBundle bundle = io.stallion.assetBundling.BundleRegistry.instance().getByPath(bundleFilePath);
090        bundle.hydrateFilesIfNeeded(true);
091        StringBuilder builder = new StringBuilder();
092        for (AssetFile af: bundle.getFiles()) {
093            af.hydrateIfNeeded(true);
094            String tag = "<script src=\"{0}\" type=\"text/javascript\"></script>";
095            if (".head.js".equals(extension)) {
096                if (empty(af.getHeadJavaScript())) {
097                    continue;
098                }
099            } else if (".css".equals(extension)) {
100                if (empty(af.getCss())) {
101                    continue;
102                }
103                tag = "<link rel=\"stylesheet\" href=\"{0}\">";
104            } else if (".js".equals(extension)) {
105                if (empty(af.getJavaScript())) {
106                    continue;
107                }
108            }
109            String url;
110            try {
111                String relativePath = af.getRelativePath();
112                if (!relativePath.startsWith("/")) {
113                    relativePath = "/" + relativePath;
114                }
115                url = Settings.instance().getCdnUrl() + "/st-resource/" + plugin + relativePath + extension + "?bundlePath=" + URLEncoder.encode(this.path, "utf-8") + "&ts=" + af.getHydratedAt();
116            } catch (UnsupportedEncodingException e) {
117                throw new RuntimeException(e);
118            }
119            builder.append("    " + MessageFormat.format(tag, url) + "\n");
120
121        }
122        return builder.toString();
123
124    }
125
126    public String renderProductionHtml() {
127        String content = renderProductionContent();
128        String hash = DigestUtils.md5Hex(content);
129        String tag = "<script src=\"{0}\" type=\"text/javascript\"></script>";
130        String url = Settings.instance().getCdnUrl() + "/st-resource/" + plugin + path + extension + "?isFullResourceBundle=true&hash=" + hash;
131        if (".css".equals(extension)) {
132            tag = "<link rel=\"stylesheet\" href=\"{0}\">";
133        }
134        return MessageFormat.format(tag, url);
135    }
136
137
138
139    public String renderProductionContent() {
140        URL url = resourceClass.getResource(path + ".min" + extension);
141        if (url == null) {
142            url = resourceClass.getResource(path + extension);
143        }
144        if (url == null) {
145            throw new UsageException("Could not find compiled bundle for path: " + path);
146        }
147        if (extension.equals(".js") || extension.equals(".head.js")) {
148            Context.response().addHeader("X-Sourcemap", "/st-resource/" + this.plugin + path + ".min" + extension + ".map");
149        }
150        try {
151            return IOUtils.toString(url, "UTF-8");
152        } catch (IOException e) {
153            throw new RuntimeException(e);
154        }
155
156    }
157}