Package jodd.htmlstapler

Source Code of jodd.htmlstapler.HtmlStaplerBundlesManager

// Copyright (c) 2003-2014, Jodd Team (jodd.org). All Rights Reserved.

package jodd.htmlstapler;

import jodd.io.FileNameUtil;
import jodd.io.FileUtil;
import jodd.io.NetUtil;
import jodd.io.ZipUtil;
import jodd.io.findfile.FindFile;
import jodd.util.Base32;
import jodd.util.CharUtil;
import jodd.util.StringBand;
import jodd.util.StringPool;
import jodd.util.StringUtil;
import jodd.util.SystemUtil;
import jodd.log.Logger;
import jodd.log.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* HTML resources bundles manager.
*/
public class HtmlStaplerBundlesManager {

  private static final Logger log = LoggerFactory.getLogger(HtmlStaplerBundlesManager.class);

  protected int bundleCount;    // counter for new bundles

  protected Map<String, String> actionBundles;       // action -> bundleId/digest
  protected Map<String, String> mirrors;          // temp id -> bundleId

  protected final String webRoot;
  protected final String contextPath;
  protected final Strategy strategy;

  // parameters
  protected String localFilesEncoding = StringPool.UTF_8;
  protected String bundleFolder;
  protected String staplerPath = "jodd-bundle";
  protected String localAddressAndPort = "http://localhost:8080";
  protected boolean downloadLocal;
  protected boolean sortResources;
  protected boolean notFoundExceptionEnabled = true;

  // ---------------------------------------------------------------- strategy

  public enum Strategy {

    /**
     * For each action manager stores the bundle id.
     * This gives top performances, as the links are collected only
     * once per page and bundle id (digest) is calculated only once
     * as well, only first time page is accessed. However, if there
     * are dynamic REST-alike links, there might be a large or infinite
     * number of action links in the application. This can be handled
     * in the code by overriding method <code>resolveRealActionPath()</code>.
     */
    ACTION_MANAGED,

    /**
     * Pragmatic strategy that collects all links and builds  bundle id
     * (digest) every time when page is processed. This gives slightly
     * slower performances, but there is no additional memory consumption.
     */
    RESOURCES_ONLY
  }

  // ---------------------------------------------------------------- init

  /**
   * Creates new instance and initialize it.
   */
  public HtmlStaplerBundlesManager(String contextPath, String webRoot, Strategy strategy) {
    this.contextPath = contextPath;
    this.webRoot = webRoot;
    this.strategy = strategy;
    this.bundleFolder = SystemUtil.getTempDir();

    if (strategy == Strategy.ACTION_MANAGED) {
      actionBundles = new HashMap<String, String>();
      mirrors = new HashMap<String, String>();
    }
  }

  /**
   * Starts bundle usage by creating new {@link BundleAction}.
   */
  public BundleAction start(String servletPath, String bundleName) {
    return new BundleAction(this, servletPath, bundleName);
  }

  // ---------------------------------------------------------------- params

  /**
   * Returns current {@link Strategy strategy}.
   */
  public Strategy getStrategy() {
    return strategy;
  }

  /**
   * Returns <code>true</code> if resources are sorted before
   * bundle id (a digest) is created. When sorting is enabled,
   * two pages will share the same bundle even if they list
   * resources in different order.
   */
  public boolean isSortResources() {
    return sortResources;
  }

  /**
   * Sets the resources sorting before bundle id (i.e. a digest)
   * is created.
   */
  public void setSortResources(boolean sortResources) {
    this.sortResources = sortResources;
  }

  /**
   * Returns current web root.
   */
  public String getWebRoot() {
    return webRoot;
  }

  /**
   * Returns bundles folder. By default, it is a system temp folder.
   */
  public String getBundleFolder() {
    return bundleFolder;
  }

  /**
   * Sets bundle folder.
   */
  public void setBundleFolder(String bundleFolder) {
    this.bundleFolder = bundleFolder;
  }

  /**
   * Returns stapler path. It is both the file system folder
   * name and the web folder name.
   */
  public String getStaplerPath() {
    return staplerPath;
  }

  /**
   * Sets stapler path.
   */
  public void setStaplerPath(String staplerPath) {
    this.staplerPath = staplerPath;
  }

  /**
   * Returns local files encoding. By default its UTF8.
   */
  public String getLocalFilesEncoding() {
    return localFilesEncoding;
  }

  /**
   * Sets local files encoding.
   */
  public void setLocalFilesEncoding(String localFilesEncoding) {
    this.localFilesEncoding = localFilesEncoding;
  }

  /**
   * Returns local address and port for downloading
   * local resources.
   */
  public String getLocalAddressAndPort() {
    return localAddressAndPort;
  }

  /**
   * Specifies local address and port for downloading
   * local resources. By default its "http://localhost:8080".
   */
  public void setLocalAddressAndPort(String localAddressAndPort) {
    this.localAddressAndPort = localAddressAndPort;
  }

  /**
   * Returns <code>true</code> if local resource files are downloaded
   * and not loaded from file system.
   */
  public boolean isDownloadLocal() {
    return downloadLocal;
  }

  /**
   * Sets if local resource files should be downloaded or loaded from file system.
   */
  public void setDownloadLocal(boolean downloadLocal) {
    this.downloadLocal = downloadLocal;
  }

  /**
   * Returns <code>true</code> if exception will be thrown when
   * resource is not found.
   */
  public boolean isNotFoundExceptionEnabled() {
    return notFoundExceptionEnabled;
  }

  /**
   * Sets if exception should be thrown when some resource is not found.
   * If not enabled, the error will be logged as a warning.
   */
  public void setNotFoundExceptionEnabled(boolean notFoundExceptionEnabled) {
    this.notFoundExceptionEnabled = notFoundExceptionEnabled;
  }

  // ---------------------------------------------------------------- lookup

  /**
   * Creates bundle file in bundleFolder/staplerPath. Only file object
   * is created, not the file content.
   */
  protected File createBundleFile(String bundleId) {
    File folder = new File(bundleFolder, staplerPath);
    if (folder.exists() == false) {
      folder.mkdirs();
    }
    return new File(folder, bundleId);
  }

  /**
   * Lookups for bundle file.
   */
  public File lookupBundleFile(String bundleId) {
    if ((mirrors != null) && (mirrors.isEmpty() == false)) {
      String realBundleId = mirrors.remove(bundleId);

      if (realBundleId != null) {
        bundleId = realBundleId;
      }
    }
    return createBundleFile(bundleId);
  }

  /**
   * Locates gzipped version of bundle file. If gzip file
   * does not exist, it will be created.
   */
  public File lookupGzipBundleFile(File file) throws IOException {
    String path = file.getPath() + ZipUtil.GZIP_EXT;
    File gzipFile = new File(path);

    if (gzipFile.exists() == false) {
      if (log.isDebugEnabled()) {
        log.debug("gzip bundle to " + path);
      }
      ZipUtil.gzip(file);
    }

    return gzipFile;
  }

  /**
   * Lookups for a bundle id for a given action.
   * Returns <code>null</code> if action still has no bundle.
   * Returns an empty string if action has an empty bundle.
   */
  public String lookupBundleId(String actionPath) {
    return actionBundles.get(actionPath);
  }

  // ---------------------------------------------------------------- register

  /**
   * Registers new, temporary bundle id for given action path.
   * This id is used on first bundle usage, later it will be replaces with
   * real bundle id.
   */
  public String registerNewBundleId() {
    bundleCount++;
    return String.valueOf(bundleCount);
  }

  /**
   * Registers new bundle that consist of provided list of source paths.
   * Returns the real bundle id, as provided one is just a temporary bundle id.
   */
  public synchronized String registerBundle(String contextPath, String actionPath, String tempBundleId, String bundleContentType, List<String> sources) {

    if (tempBundleId == null || sources.isEmpty()) {
      if (strategy == Strategy.ACTION_MANAGED) {
        // page does not include any resource source file
        actionBundles.put(actionPath, StringPool.EMPTY);
      }
      return null;
    }

    // create unique digest from the collected sources
    String[] sourcesArray = sources.toArray(new String[sources.size()]);
    for (int i = 0, sourcesArrayLength = sourcesArray.length; i < sourcesArrayLength; i++) {
      sourcesArray[i] = sourcesArray[i].trim().toLowerCase();
    }
    if (sortResources) {
      Arrays.sort(sourcesArray);
    }

    StringBand sb = new StringBand(sourcesArray.length);
    for (String src : sourcesArray) {
      sb.append(src);
    }
    String sourcesString = sb.toString();

    String bundleId = createDigest(sourcesString);
    bundleId += '.' + bundleContentType;

    // bundle appears for the first time, create the bundle
    if (strategy == Strategy.ACTION_MANAGED) {
      actionBundles.put(actionPath, bundleId);
      mirrors.put(tempBundleId, bundleId);
    }
    try {
      createBundle(contextPath, actionPath, bundleId, sources);
    } catch (IOException ioex) {
      throw new HtmlStaplerException("Can't create bundle", ioex);
    }
    return bundleId;
  }

  /**
   * Creates digest i.e. bundle id from given string.
   * Returned digest must be filename safe, for all platforms.
   */
  protected String createDigest(String source) {
    MessageDigest shaDigester;
    try {
      shaDigester = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException nsaex) {
      throw new HtmlStaplerException(nsaex);
    }

    byte[] bytes = shaDigester.digest(CharUtil.toSimpleByteArray(source));
    return Base32.encode(bytes);
  }

  /**
   * Creates bundle file by loading resource files content. If bundle file already
   * exist it will not be recreated!
   */
  protected void createBundle(String contextPath, String actionPath, String bundleId, List<String>sources) throws IOException {
    File bundleFile = createBundleFile(bundleId);
    if (bundleFile.exists()) {
      return;
    }

    StringBand sb = new StringBand(sources.size() * 2);
    for (String src : sources) {
      if (sb.length() != 0) {
        sb.append(StringPool.NEWLINE);
      }
      String content;
      if (isExternalResource(src)) {
        try {
          content = NetUtil.downloadString(src, localFilesEncoding);
        } catch (IOException ioex) {
          if (notFoundExceptionEnabled) {
            throw ioex;
          }
          if (log.isWarnEnabled()) {
            log.warn("Download failed: " + src + "; " + ioex.getMessage());
          }
          content = null;
        }
      } else {
        if (downloadLocal == false) {
          // load local resource from file system
          String localFile = webRoot;

          if (src.startsWith(contextPath + '/')) {
            src = src.substring(contextPath.length());
          }

          if (src.startsWith(StringPool.SLASH)) {
            // absolute path
            localFile += src;
          } else {
            // relative path
            localFile += '/' + FileNameUtil.getPathNoEndSeparator(actionPath) + '/' + src;
          }

          // trim link parameters, if any
          int qmndx = localFile.indexOf('?');
          if (qmndx != -1) {
            localFile = localFile.substring(0, qmndx);
          }

          try {
            content = FileUtil.readString(localFile);
          } catch (IOException ioex) {
            if (notFoundExceptionEnabled) {
              throw ioex;
            }
            if (log.isWarnEnabled()) {
              log.warn(ioex.getMessage());
            }
            content = null;
          }
        } else {
          // download local resource
          String localUrl = localAddressAndPort;

          if (src.startsWith(StringPool.SLASH)) {
            localUrl += contextPath + src;
          } else {
            localUrl += contextPath + FileNameUtil.getPath(actionPath) + '/' + src;
          }

          try {
            content = NetUtil.downloadString(localUrl, localFilesEncoding);
          } catch (IOException ioex) {
            if (notFoundExceptionEnabled) {
              throw ioex;
            }
            if (log.isWarnEnabled()) {
              log.warn("Download failed: " + localUrl + "; " + ioex.getMessage());
            }
            content = null;
          }
        }

        if (content != null) {
          if (isCssResource(src)) {
            content = fixCssRelativeUrls(content, src);
          }
        }
      }

      if (content != null) {
        content = onResourceContent(content);
        sb.append(content);
      }
    }

    FileUtil.writeString(bundleFile, sb.toString());

    if (log.isInfoEnabled()) {
      log.info("Bundle created: " + bundleId);
    }
  }

  /**
   * Returns <code>true</code> if resource link has to be downloaded.
   * By default, if resource link starts with "http://" or with "https://"
   * it will be considered as external resource.
   */
  protected boolean isExternalResource(String link) {
    return link.startsWith("http://") || (link.startsWith("https://"));
  }

  /**
   * Invoked before resource content is stored in the bundle.
   * May be us used for additional resource processing, such as
   * compressing, cleaning etc. By default it just returns unmodified
   * content.
   */
  protected String onResourceContent(String content) {
    return content;
  }

  // ---------------------------------------------------------------- url rewriting

  /**
   * Resolves real action path for given one.
   * When some URLs are dynamically created, many different links points
   * to the same page. Use this to prevent memory leaking.
   */
  protected String resolveRealActionPath(String actionPath) {
    return actionPath;
  }

  // ---------------------------------------------------------------- reset

  /**
   * Clears all settings and removes all created bundle files from file system.
   */
  public synchronized void reset() {
    if (strategy == Strategy.ACTION_MANAGED) {
      actionBundles.clear();
      mirrors.clear();
    }

    FindFile ff = new FindFile();
    ff.setIncludeDirs(false);
    ff.searchPath(new File(bundleFolder, staplerPath));

    File f;
    int count = 0;
    while ((f = ff.nextFile()) != null) {
      f.delete();
      count++;
    }
    if (log.isInfoEnabled()) {
      log.info("reset: " + count + " bundle files deleted.");
    }
  }

  // ---------------------------------------------------------------- css related

  /**
   * Returns <code>true</code> if resource is CSS, so the
   * CSS urls can be fixed.
   */
  protected boolean isCssResource(String src) {
    return src.endsWith(".css");
  }

  private static final Pattern CSS_URL_PATTERN = Pattern.compile("url\\s*\\(\\s*([^\\)\\s]*)\\s*\\)", Pattern.CASE_INSENSITIVE);

  /**
   * Returns the content with all relative URLs fixed.
   */
  protected String fixCssRelativeUrls(String content, String src) {

    String path = FileNameUtil.getPath(src);

    Matcher matcher = CSS_URL_PATTERN.matcher(content);

    StringBuilder sb = new StringBuilder(content.length());

    int start = 0;

    while (matcher.find()) {
      sb.append(content.substring(start, matcher.start()));

      String url = fixRelativeUrl(matcher.group(1), path);

      sb.append(url);

      start = matcher.end();
    }

    sb.append(content.substring(start));

    return sb.toString();
  }

  /**
   * For a given URL (optionally quoted), produces CSS URL
   * where relative paths are fixed and prefixed with offsetPath.
   */
  protected String fixRelativeUrl(String url, String offsetPath) {

    url = StringUtil.removeChars(url, "'\"");   // remove quotes

    StringBuilder res = new StringBuilder();
    res.append("url('");

    if (url.startsWith(StringPool.SLASH) == false) {
      res.append("../");
      res.append(offsetPath);
    }

    res.append(url);
    res.append("')");

    return res.toString();
  }

}
TOP

Related Classes of jodd.htmlstapler.HtmlStaplerBundlesManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.