/*
 * Copyright (c) 2008. All rights reserved.
 */
package ro.isdc.wro.util;

import static org.apache.commons.io.IOUtils.closeQuietly;
import static org.apache.commons.lang3.Validate.notNull;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Properties;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.FastDateFormat;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.factory.WroModelFactory;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.locator.ServletContextUriLocator;
import ro.isdc.wro.model.resource.processor.ResourcePostProcessor;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;

/**
 * Utility class.
 *
 * @author Alex Objelean
 * @created Created on Nov 13, 2008
 */
public final class WroUtil {
  private static final String SEPARATOR_UNIX = "/";
  private static final String SEPARATOR_WINDOWS = "\\";
  /**
   * Empty line pattern.
   */
  public static final Pattern EMTPY_LINE_PATTERN = Pattern.compile(loadRegexpWithKey("emptyLine"), Pattern.MULTILINE);
  /**
   * Thread safe date format used to transform milliseconds into date as string to put in response header. The localy is
   * set explicitly to US to conform to specification.
   */
  private static final FastDateFormat DATE_FORMAT = FastDateFormat.getInstance("E, dd MMM yyyy HH:mm:ss z",
      TimeZone.getTimeZone("GMT"), Locale.US);
  /**
   * Patterns used to search for mangled Accept-Encoding header.
   */
  private static final Pattern PATTERN_ACCEPT_ENCODING = Pattern.compile(loadRegexpWithKey("requestHeader.acceptEncoding"));
  private static final Pattern PATTERN_GZIP = Pattern.compile(loadRegexpWithKey("requestHeader.gzip"));

  private static final AtomicInteger threadFactoryNumber = new AtomicInteger(1);
  public static final InputStream EMPTY_STREAM = new ByteArrayInputStream("".getBytes());

  /**
   * @return {@link ThreadFactory} with daemon threads.
   */
  public static ThreadFactory createDaemonThreadFactory(final String name) {
    return new ThreadFactory() {
      private final String prefix = "wro4j-" + name + "-" + threadFactoryNumber.getAndIncrement() + "-thread-";
      private final AtomicInteger threadNumber = new AtomicInteger(1);

      public Thread newThread(final Runnable runnable) {
        final Thread thread = new Thread(runnable, prefix + threadNumber.getAndIncrement());
        thread.setDaemon(true);
        return thread;
      }
    };
  }

  /**
   * Transforms milliseconds into date format for response header of this form: Sat, 10 Apr 2010 17:31:31 GMT.
   *
   * @param milliseconds
   *          to transform
   * @return string representation of the date.
   */
  public static String toDateAsString(final long milliseconds) {
    return DATE_FORMAT.format(milliseconds);
  }

  /**
   * Retrieve pathInfo from a given location.
   *
   * @param request
   * @param location
   *          where to search contextPath.
   * @return pathInfo value.
   */
  public static String getPathInfoFromLocation(final HttpServletRequest request, final String location) {
    if (StringUtils.isEmpty(location)) {
      throw new IllegalArgumentException("Location cannot be empty string!");
    }
    final String contextPath = request.getContextPath();
    if (contextPath != null) {
      if (startsWithIgnoreCase(location, contextPath)) {
        return location.substring(contextPath.length());
      } else {
        return location;
      }
    }
    final String noSlash = location.substring(1);
    final int nextSlash = noSlash.indexOf('/');
    if (nextSlash == -1) {
      return "";
    }
    return noSlash.substring(nextSlash);
  }

  /**
   *

   * Case insensitive check if a String starts with a specified prefix.
   *

   *

   * nulls are handled without exceptions. Two null references are considered to be equal. The
   * comparison is case insensitive.
   *

   *
   *

   * StringUtils.startsWithIgnoreCase(null, null)      = true
   * StringUtils.startsWithIgnoreCase(null, "abcdef")  = false
   * StringUtils.startsWithIgnoreCase("abc", null)     = false
   * StringUtils.startsWithIgnoreCase("abc", "abcdef") = true
   * StringUtils.startsWithIgnoreCase("abc", "ABCDEF") = true
   *

   *
   * @see java.lang.String#startsWith(String)
   * @param str
   *          the String to check, may be null
   * @param prefix
   *          the prefix to find, may be null
   * @return true if the String starts with the prefix, case insensitive, or both null
   * @since 2.4
   */
  public static boolean startsWithIgnoreCase(final String str, final String prefix) {
    return startsWith(str, prefix, true);
  }

  /**
   * Creates a folder like implementation for a class. Ex: com.mycompany.MyClass -> com/mycompany/
   *
   * @param clazz
   *          used as a base location for determining the package path.
   * @return a string representation of the path where the class resides.
   */
  public static String toPackageAsFolder(final Class<?> clazz) {
    Validate.notNull(clazz, "Class cannot be null!");
    return clazz.getPackage().getName().replace('.', '/');
  }

  /**
   *

   * Check if a String starts with a specified prefix (optionally case insensitive).
   *

   *
   * @see java.lang.String#startsWith(String)
   * @param str
   *          the String to check, may be null
   * @param prefix
   *          the prefix to find, may be null
   * @param ignoreCase
   *          indicates whether the compare should ignore case (case insensitive) or not.
   * @return true if the String starts with the prefix or both null
   */
  private static boolean startsWith(final String str, final String prefix, final boolean ignoreCase) {
    if (str == null || prefix == null) {
      return (str == null && prefix == null);
    }
    if (prefix.length() > str.length()) {
      return false;
    }
    return str.regionMatches(ignoreCase, 0, prefix, 0, prefix.length());
  }

  /**
   * Retrieve servletPath from a given location.
   *
   * @param location
   *          where to search the servletPath.
   * @return ServletPath string value.
   */
  public static String getServletPathFromLocation(final HttpServletRequest request, final String location) {
    return location.replace(getPathInfoFromLocation(request, location), StringUtils.EMPTY);
  }

  /**
   * Analyze headers of the request and searches for mangled (by proxy) for "Accept-Encoding" header and its mangled
   * variations and gzip header value and its mangled variations.
   *
   * @return true if this request support gzip encoding.
   */
  @SuppressWarnings("unchecked")
  public static boolean isGzipSupported(final HttpServletRequest request) {
    if (request != null) {
      final Enumeration<String> headerNames = request.getHeaderNames();
      if (headerNames != null) {
        while (headerNames.hasMoreElements()) {
          final String headerName = headerNames.nextElement();
          final Matcher m = PATTERN_ACCEPT_ENCODING.matcher(headerName);
          if (m.find()) {
            final String headerValue = request.getHeader(headerName);
            final Matcher mValue = PATTERN_GZIP.matcher(headerValue);
            return mValue.find();
          }
        }
      }
    }
    return false;
  }

  /**
   * Transforms a java multi-line string into javascript multi-line string. This technique was found at {@link http
   * ://stackoverflow.com/questions/805107/multiline-strings-in-javascript/}
   *
   * @param data
   *          a string containing new lines.
   * @return a string which being evaluated on the client-side will be treated as a correct multi-line string.
   */
  public static String toJSMultiLineString(final String data) {
    final StringBuffer result = new StringBuffer("[");
    if (data != null) {
      final String[] lines = data.split("\n");
      if (lines.length == 0) {
        result.append("\"\"");
      }
      for (int i = 0; i < lines.length; i++) {
        final String line = lines[i];
        result.append("\"");
        result.append(line.replace(SEPARATOR_WINDOWS, "\\\\").replace("\"", "\\\"").replaceAll("\\r|\\n", ""));
        // this is used to force a single line to have at least one new line (otherwise cssLint fails).
        if (lines.length == 1) {
          result.append("\\n");
        }
        result.append("\"");
        if (i < lines.length - 1) {
          result.append(",");
        }
      }
    }
    result.append("].join(\"\\n\")");
    return result.toString();
  }

  /**
   * Utility used to verify that requestURI matches provided path
   */
  public static boolean matchesUrl(final HttpServletRequest request, final String path) {
    final Pattern pattern = Pattern.compile(".*" + path + "[/]?", Pattern.CASE_INSENSITIVE);
    if (request.getRequestURI() != null) {
      final Matcher m = pattern.matcher(request.getRequestURI());
      return m.matches();
    }
    return false;
  }

  /**
   * A factory method for creating a {@link ResourceProcessor} based on provided {@link ResourcePreProcessor}.
   *
   * @param preProcessor
   *          {@link ResourcePreProcessor} to use as a {@link ResourceProcessor}.
   * @return instance of {@link ResourceProcessor}.
   */
  public static ResourcePostProcessor newResourceProcessor(final Resource resource,
      final ResourcePreProcessor preProcessor) {
    return new ResourcePostProcessor() {
      public void process(final Reader reader, final Writer writer)
          throws IOException {
        preProcessor.process(resource, reader, writer);
      }
    };
  }

  /**
   * A simple way to create a {@link WroModelFactory}.
   *
   * @param model
   *          {@link WroModel} instance to be returned by the factory.
   */
  public static WroModelFactory factoryFor(final WroModel model) {
    return new WroModelFactory() {
      public WroModel create() {
        return model;
      }

      public void destroy() {
      }
    };
  }

  public static <T> ObjectFactory<T> simpleObjectFactory(final T object) {
    return new ObjectFactory<T>() {
      public T create() {
        return object;
      }
    };
  }

  /**
   * Load the regular expression stored in in regexp.properties resource file.
   *
   * @param key
   *          the key of the regexp to load.
   * @return regular expression value.
   */
  public static String loadRegexpWithKey(final String key) {
    InputStream stream = null;
    try {
      stream = WroUtil.class.getResourceAsStream("regexp.properties");
      final Properties props = new RegexpProperties().load(stream);
      return props.getProperty(key);
    } catch (final IOException e) {
      throw new WroRuntimeException("Could not load pattern with key: " + key + " from property file", e);
    } finally {
      closeQuietly(stream);
    }
  }

  /**
   * @return the implementation version of wro4j.
   */
  public static String getImplementationVersion() {
    return WroUtil.class.getPackage().getImplementationVersion();
  }

  /**
   * Copy and close the reader and writer streams.
   *
   * @param reader
   *          The source stream.
   * @param writer
   *          The destination stream.
   * @throws IOException
   *           If content cannot be copy.
   */
  public static void safeCopy(final Reader reader, final Writer writer)
      throws IOException {
    try {
      IOUtils.copy(reader, writer);
    } finally {
      IOUtils.closeQuietly(reader);
      IOUtils.closeQuietly(writer);
    }
  }

  /**
   * @return a generated {@link File} with unique name located in temp folder.
   */
  public static File createTempFile() {
    return createTempFile("temp");
  }

  /**
   * @return a folder with unique name..
   */
  public static File createTempDirectory() {
    final String fileName = String.format("wro4j-%s", UUID.randomUUID().toString());
    final File file = new File(FileUtils.getTempDirectory(), fileName);
    file.mkdir();
    return file;
  }

  /**
   * Creates a temp file which has a certain extension.
   *
   * @param extension
   *          of the created temp file.
   */
  public static File createTempFile(final String extension) {
    try {
      final String fileName = String.format("wro4j-%s.%s", UUID.randomUUID().toString(), extension);
      final File file = new File(createTempDirectory(), fileName);
      file.createNewFile();
      return file;
    } catch (final IOException e) {
      throw WroRuntimeException.wrap(e);
    }
  }

  /**
   * Join two paths (using unix separator) and make sure to use exactly one separator (by adding or removing one if required).
   */
  public static String joinPath(final String left, final String right) {
    String leftHand = left;
    if (!left.endsWith(SEPARATOR_UNIX)) {
      leftHand += SEPARATOR_UNIX;
    }
    return leftHand + right.replaceFirst("^/(.*)", "$1");
  }

  /**
   * Cleans the image url by trimming result and removing \' or \" characters if such exists.
   *
   * @param imageUrl
   *          to clean.
   * @return cleaned image URL.
   */
  public static final String cleanImageUrl(final String imageUrl) {
    notNull(imageUrl);
    return imageUrl.replace('\'', ' ').replace('\"', ' ').trim();
  }

  /**
   * Removes the query string from the provided path (everything followed by '?' including the question mark).
   */
  public static final String removeQueryString(final String path) {
    return path.replaceFirst("\\?.*", StringUtils.EMPTY);
  }

  /**
   * @return current working directory
   */
  public static final File getWorkingDirectory() {
    return new File(System.getProperty("user.dir"));
  }

  /**
   * Similar to {@link FilenameUtils#getFullPath(String)}, but fixes the problem with Windows platform for situations
   * when the path starts with "/" (servlet context relative resources) which are resolved to "\" on windows.
   *
   * @param path
   *          to compute filePath from.
   * @return full path from the provided path.
   */
  public static final String getFullPath(final String path) {
    final String fullPath = FilenameUtils.getFullPath(path);
    return replaceWithServletContextSeparatorIfNedded(fullPath);
  }

  /**
   * Similar to {@link FilenameUtils#normalize(String)}, but fixes the problem with Windows platform for situations
   * when the path starts with "/" (servlet context relative resources) which are resolved to "\" on windows.
   *
   * @param path
   *          to compute filePath from.
   * @return full path from the provided path.
   */
  public static final String normalize(final String path) {
    final String normalized = FilenameUtils.normalize(path);
    return replaceWithServletContextSeparatorIfNedded(normalized);
  }

  private static String replaceWithServletContextSeparatorIfNedded(String normalized) {
    if (normalized.startsWith(SEPARATOR_WINDOWS) || normalized.contains(SEPARATOR_WINDOWS)) {
      normalized = normalized.replace(SEPARATOR_WINDOWS, ServletContextUriLocator.PREFIX);
    }
    return normalized;
  }
}


package ro.isdc.wro.http.support;

import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.config.jmx.WroConfiguration;
import ro.isdc.wro.http.WroFilter;
import ro.isdc.wro.http.WroServletContextListener;
import ro.isdc.wro.manager.factory.WroManagerFactory;

/**
 * Encapsulates the details of storing/retrieving attributes from {@link ServletContext}.

 * This class know how to handle multiple {@link WroFilter} & {@link WroServletContextListener} configurations, by
 * setting the attributes with a unique key computed based on provided name (using
 * {@link ServletContextAttributeHelper#getName()}. Use this class to retrieve wro4j related attributes, instead of
 * hard-coding the name of the attribute to retrieve.
 *

 * Usage scenario:
 *

 *
This is the most common use-case, when there is a single {@link WroFilter} and a
 * {@link WroServletContextListener} configured in web.xml. In order to retrieve/store attributes in servlet context
 * use:
 *
 *

 * {@link ServletContextAttributeHelper} helper = new {@link ServletContextAttributeHelper}(servletContext);
 * {@link WroConfiguration} config = helper.getWroConfiguration();
 * {@link WroManagerFactory} managerFactory = helper.getManagerFactory();
 *

 *
 *

 *
When there are multiple {@link WroFilter} configurations in web.xml (each should have a dedicated listener
 * extended form {@link WroServletContextListener}). In order to retrieve/store attributes in servlet context use:
 *
 *

 *       String name = ..//the name of the filter (defined in init-param called name).
 *       {@link ServletContextAttributeHelper} helper = new {@link ServletContextAttributeHelper}(servletContext, name);
 *       {@link WroConfiguration} config = helper.getWroConfiguration();
 *       {@link WroManagerFactory} managerFactory = helper.getManagerFactory();
 *

 *
 *

 *
When there are multiple {@link WroFilter} configurations in web.xml, you can retrieve/store attributes from
 * within a filter using:
 *
 *

 * {@link ServletContextAttributeHelper} helper = {@link ServletContextAttributeHelper}.create(filterConfig);
 * {@link WroConfiguration} config = helper.getWroConfiguration();
 * {@link WroManagerFactory} managerFactory = helper.getManagerFactory();
 *

 *
 * This example is similar to previous, except that the name of the filter will be extracted in the factory method.

 *

 *
 * @author Alex Objelean
 * @created 7 May 2012
 * @since 1.4.6
 */
public class ServletContextAttributeHelper {
  private static final Logger LOG = LoggerFactory.getLogger(WroServletContextListener.class);
  /**
   * The name of the init param used to retrieve the name of the filter. This is useful when you want to have multiple
   * filter declarations and a lister (of {@link WroServletContextListener} type) associated for each filter. This way
   * you can ensure that each filter will use its own configurations.
   * @VisibleForTesting
   */
  static final String INIT_PARAM_NAME = "name";
  /**
   * Default value of the name init param. This one is used when no value is defined.
   */
  public static final String DEFAULT_NAME = "default";
  /**
   * {@link ServletContext} where the attributes are stored.
   */
  private ServletContext servletContext;
  /**
   * Unique name associated with servletContext, required for multiple listener support.
   */
  private String name;

  /**
   * Supported attributes.
   * @VisibleForTesting
   */
  static enum Attribute {
    CONFIGURATION(WroConfiguration.class), MANAGER_FACTORY(WroManagerFactory.class);
    private Class<?> type;

    private Attribute(final Class<?> type) {
      this.type = type;
    }

    /**
     * @return true if the object is a valid subtype.
     */
    boolean isValid(final Object object) {
      return object == null || type.isAssignableFrom(object.getClass());
    }
  }

  /**
   * Uses default name for storing/retrieving attributes in {@link ServletContext}.
   */
  public ServletContextAttributeHelper(final ServletContext servletContext) {
    this(servletContext, DEFAULT_NAME);
  }

  /**
   * Factory method which uses default name for storing/retrieving attributes in {@link ServletContext}.
   */
  public static ServletContextAttributeHelper create(final FilterConfig filterConfig) {
    Validate.notNull(filterConfig);
    final String nameFromParam = filterConfig.getInitParameter(INIT_PARAM_NAME);
    final String name = nameFromParam != null ? nameFromParam : DEFAULT_NAME;
    return new ServletContextAttributeHelper(filterConfig.getServletContext(), name);
  }

  /**
   * Uses default name to work with {@link ServletContext} attribute.
   *
   * @param servletContext
   *          a not null {@link ServletContext} object.
   * @param name
   *          of the listener for which the helper will be used.
   */
  public ServletContextAttributeHelper(final ServletContext servletContext, final String name) {
    Validate.notNull(servletContext);
    Validate.notBlank(name);
    this.servletContext = servletContext;
    this.name = name;
    LOG.debug("initializing attributeHelper named: {}", name);
  }

  /**
   * Creates a unique name used as a key to store the attribute in {@link ServletContext}.
   *
   * @param attribute
   *          type of attribute.
   * @return the name of the attribute used to store in servlet context.
   * @VisibleForTesting
   */
  final String getAttributeName(final Attribute attribute) {
    Validate.notNull(attribute);
    return WroServletContextListener.class.getName() + "-" + attribute.name() + "-" + this.name;
  }

  /**
   * @return the value of the attribute stored in {@link ServletContext} for this listener.
   * @VisibleForTesting
   */
  final Object getAttribute(final Attribute attribute) {
    Validate.notNull(attribute);
    return servletContext.getAttribute(getAttributeName(attribute));
  }

  /**
   * @return the {@link WroManagerFactory} stored in servletContext.
   */
  public WroManagerFactory getManagerFactory() {
    return (WroManagerFactory) getAttribute(Attribute.MANAGER_FACTORY);
  }

  /**
   * @return the {@link WroConfiguration} stored in servletContext.
   */
  public WroConfiguration getWroConfiguration() {
    return (WroConfiguration) getAttribute(Attribute.CONFIGURATION);
  }

  /**
   * Sets the attribute into the servlet context. The name of the attribute will be computed for you.
   */
  final void setAttribute(final Attribute attribute, final Object object) {
    Validate.notNull(attribute);
    LOG.debug("setting attribute: {} with value: {}", attribute, object);
    Validate.isTrue(attribute.isValid(object), object + " is not of valid subType for attribute: " + attribute);
    servletContext.setAttribute(getAttributeName(attribute), object);
  }

  /**
   * Set the configuration object as a servletContext attribute.
   */
  public void setWroConfiguration(final WroConfiguration config) {
    setAttribute(Attribute.CONFIGURATION, config);
  }

  /**
   * Set the configuration object as a servletContext attribute.
   */
  public void setManagerFactory(final WroManagerFactory managerFactory) {
    setAttribute(Attribute.MANAGER_FACTORY, managerFactory);
  }

  /**
   * Remove all attributes from {@link ServletContext}.
   */
  public void clear() {
    LOG.debug("destroying servletContext: {}", this.name);
    for (Attribute attribute : Attribute.values()) {
      servletContext.removeAttribute(getAttributeName(attribute));
    }
  }

  /**
   * @VisibleForTesting
   * @return the name of the listener used by this helper.
   */
  final String getName() {
    return name;
  }
}


package ro.isdc.wro.model.resource.processor.support;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;

import org.apache.commons.io.IOUtils;

import ro.isdc.wro.http.support.ContentTypeResolver;
import ro.isdc.wro.util.Base64;

/**
 * Generator for Data URIs.
 *
 * (Inspired from: http://github.com/nzakas/cssembed.)
 *
 * @author Alex Objelean
 * @author Ivar Conradi Østhus
 * @created May 7, 2010
 */
public class DataUriGenerator {
  private static final String DATA_URI_PREFIX = "data:";

  /**
   * Generate the dataUri as string associated to the passed InputStream with encoding & type based on provided
   * fileName.
   */
  public String generateDataURI(final InputStream inputStream, final String fileName)
      throws IOException {
    final StringWriter writer = new StringWriter();
    final byte[] bytes = IOUtils.toByteArray(inputStream);
    inputStream.close();

    final String mimeType = getMimeType(fileName);
    // actually write
    generateDataURI(bytes, writer, mimeType);

    return writer.toString();
  }

  private String getMimeType(final String fileName) {
    return ContentTypeResolver.get(fileName, "UTF-8").replaceAll(" ", "");
  }

  /**
   * Check if the url is actually a dataUri (base64 encoded value).
   *
   * @param url
   *          to check
   * @return true if the url is a base64 encoded value.
   */
  public static boolean isDataUri(final String url) {
    return url.startsWith(DATA_URI_PREFIX);
  }

  /**
   * Generates a data URI from a byte array and outputs to the given writer.
   *
   * @param bytes
   *          The array of bytes to output to the data URI.
   * @param out
   *          Where to output the data URI.
   * @param mimeType
   *          The MIME type to specify in the data URI.
   * @throws java.io.IOException
   */
  private void generateDataURI(final byte[] bytes, final Writer out, final String mimeType)
      throws IOException {
    // create the output
    final StringBuffer buffer = new StringBuffer();
    buffer.append(DATA_URI_PREFIX);

    // add MIME type
    buffer.append(mimeType);

    // output base64-encoding
    buffer.append(";base64,");
    buffer.append(Base64.encodeBytes(bytes));

    // output to writer
    out.write(buffer.toString());
  }
}


package ro.isdc.wro.extensions.support.lint;

import static org.apache.commons.lang3.Validate.notNull;

import java.util.ArrayList;
import java.util.Collection;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.extensions.processor.support.csslint.CssLintError;
import ro.isdc.wro.extensions.processor.support.linter.LinterError;
import ro.isdc.wro.util.Function;

/**
 * Builds an XML report for lint errors based on provided collection of {@link LinterError}'s.
 *
 * @author Alex Objelean
 * @since 1.5.0
 * @created 15 Sep 2012
 */
public class ReportXmlFormatter
    extends AbstractReportXmlFormatter<LintItem> {
  private static final Logger LOG = LoggerFactory.getLogger(ReportXmlFormatter.class);
  /**
   * Checkstyle related constants
   */
  private static final String ELEMENT_ERROR = "error";
  private static final String ATTR_COLUMN = "column";
  private static final String ATTR_MESSAGE = "message";
  /**
   * Lint related constants
   */
  private static final String ELEMENT_ISSUE = "issue";
  private static final String ELEMENT_FILE = "file";
  private static final String ATTR_NAME = "name";
  private static final String ATTR_REASON = "reason";
  private static final String ATTR_LINE = "line";
  private static final String ATTR_EVIDENCE = "evidence";
  private static final String ATTR_CHARACTER = "char";
  private static final String ATTR_SEVERITY = "severity";

  private final FormatterType formatterType;

  public static enum FormatterType {
    LINT("lint"), CHECKSTYLE("checkstyle"), CSSLINT("csslint"), JSLINT("jslint");
    private String rootElementName;

    FormatterType(final String rootElementName) {
      this.rootElementName = rootElementName;
    }

    /**
     * @param format
     *          of the {@link FormatterType} to return.
     * @return the {@link FormatterType} of searched format type or null if such format does not exist.
     */
    public static FormatterType getByFormat(final String format) {
      for (final FormatterType type : FormatterType.values()) {
        if (type.getFormat().equals(format)) {
          return type;
        }
      }
      return null;
    }

    /**
     * @return supported formats as CSV.
     */
    public static String getSupportedFormatsAsCSV() {
      final StringBuffer sb = new StringBuffer();
      for (final FormatterType type : FormatterType.values()) {
        sb.append(type.getFormat()).append(",");
      }
      return StringUtils.removeEnd(sb.toString(), ",");
    }

    /**
     * @return string representation of the format.
     */
    public String getFormat() {
      return String.format("%s-xml", this.rootElementName);
    }
  }

  /**
   * Factory method for creating {@link ReportXmlFormatter}.
   *
   * @param lintReport
   *          {@link LintReport} to build xml reports from.
   */
  public static ReportXmlFormatter create(final LintReport<LintItem> lintReport, final FormatterType formatterType) {
    return new ReportXmlFormatter(lintReport, formatterType);
  }

  public static ReportXmlFormatter createForLinterError(final LintReport<LinterError> lintReport,
      final FormatterType formatterType) {
    return createInternal(lintReport, formatterType, new Function<LinterError, LintItem>() {
      @Override
      public LintItem apply(final LinterError input)
          throws Exception {
        notNull(input);
        return new LintItemAdapter(input);
      }
    });
  }

  public static ReportXmlFormatter createForCssLintError(final LintReport<CssLintError> lintReport,
      final FormatterType formatterType) {
    return createInternal(lintReport, formatterType, new Function<CssLintError, LintItem>() {
      @Override
      public LintItem apply(final CssLintError input)
          throws Exception {
        notNull(input);
        return new LintItemAdapter(input);
      }
    });
  }

  /**
   * Creates a report which handles the adaptation of type <F> to {@link LintItem}.
   *
   * @param lintReport
   *          {@link LintReport} containing all lints.
   * @param formatterType
   *          the type of formatter to use.
   * @param adapter
   *          a {@link Function} responsible for adapting a type <F> into {@link LintItem}
   */
  private static <F> ReportXmlFormatter createInternal(final LintReport<F> lintReport,
      final FormatterType formatterType, final Function<F, LintItem> adapter) {
    Validate.notNull(lintReport);
    final LintReport<LintItem> report = new LintReport<LintItem>();
    for (final ResourceLintReport<F> reportItem : lintReport.getReports()) {
      final Collection<LintItem> lintItems = new ArrayList<LintItem>();
      for (final F lint : reportItem.getLints()) {
        try {
          LOG.debug("Adding lint: {}", lint);
          lintItems.add(adapter.apply(lint));
        } catch (final Exception e) {
          throw WroRuntimeException.wrap(e, "Problem while adapting lint item");
        }
      }
      report.addReport(ResourceLintReport.create(reportItem.getResourcePath(), lintItems));
    }
    return new ReportXmlFormatter(report, formatterType);
  }

  /**
   * @param lintReport
   *          a not null collection of {@link LinterError} used to build an XML report from.
   */
  protected ReportXmlFormatter(final LintReport<LintItem> lintReport, final FormatterType type) {
    super(lintReport);
    notNull(type);
    this.formatterType = type;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void buildDocument() {
    final Element rootElement = getDocument().createElement(formatterType.rootElementName);
    getDocument().appendChild(rootElement);

    for (final ResourceLintReport<LintItem> resourceErrors : getLintReport().getReports()) {
      rootElement.appendChild(createFileElement(resourceErrors));
    }
  }

  /**
   * Creates a {@link Node} containing informations about all errors associated to a single resource.
   */
  private Node createFileElement(final ResourceLintReport<LintItem> resourceErrors) {
    final Element fileElement = getDocument().createElement(ELEMENT_FILE);
    fileElement.setAttribute(ATTR_NAME, resourceErrors.getResourcePath());
    for (final LintItem error : resourceErrors.getLints()) {
      fileElement.appendChild(createIssueElement(error));
    }
    return fileElement;
  }

  /**
   * Creates a {@link Node} containing a detailed description of an issue.
   */
  private Node createIssueElement(final LintItem error) {
    final Element issueElement = getDocument().createElement(getIssueElementName());

    final String column = String.valueOf(error.getColumn());
    if (StringUtils.isNotBlank(column)) {
      issueElement.setAttribute(getColumnAttributeName(), column);
    }

    final String evidence = error.getEvidence();
    if (StringUtils.isNotBlank(evidence)) {
      issueElement.setAttribute(ATTR_EVIDENCE, evidence);
    }

    final String line = String.valueOf(error.getLine());
    if (StringUtils.isNotBlank(line)) {
      issueElement.setAttribute(ATTR_LINE, line);
    }

    final String reason = error.getReason();
    if (StringUtils.isNotBlank(reason)) {
      issueElement.setAttribute(getReasonAttributeName(), reason);
    }

    final String severity = error.getSeverity();
    if (StringUtils.isNotBlank(severity)) {
      issueElement.setAttribute(ATTR_SEVERITY, severity);
    }
    return issueElement;
  }

  /**
   * @return the name of the attribute indicating the character number where the issue is located.
   */
  protected String getColumnAttributeName() {
    return formatterType == FormatterType.CHECKSTYLE ? ATTR_COLUMN : ATTR_CHARACTER;
  }

  /**
   * @return the name of the attribute indicating a reason of the issue.
   */
  protected String getReasonAttributeName() {
    return formatterType == FormatterType.CHECKSTYLE ? ATTR_MESSAGE : ATTR_REASON;
  }

  /**
   * @return name of tag indicating an issue.
   */
  protected String getIssueElementName() {
    return formatterType == FormatterType.CHECKSTYLE ? ELEMENT_ERROR : ELEMENT_ISSUE;
  }
}


/*
 * Copyright (c) 2010. All rights reserved.
 */
package ro.isdc.wro.config;

import static org.apache.commons.lang3.Validate.notNull;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;

import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.config.jmx.WroConfiguration;
import ro.isdc.wro.config.support.ContextPropagatingCallable;
import ro.isdc.wro.http.WroFilter;

/**
 * Holds the properties related to a request cycle.
 *
 * @author Alex Objelean
 */
public class Context
    implements ReadOnlyContext {
  /**
   * Maps correlationId with a Context.
   */
  private static final Map<String, Context> CONTEXT_MAP = Collections.synchronizedMap(new HashMap<String, Context>());
  /**
   * Holds a correlationId, created in {@link WroFilter}. A correlationId will be associated with a {@link Context}
   * object.
   */
  private static ThreadLocal<String> CORRELATION_ID = new ThreadLocal<String>();
  private WroConfiguration config;
  /**
   * Request.
   */
  private transient HttpServletRequest request;
  /**
   * Response.
   */
  private transient HttpServletResponse response;
  /**
   * ServletContext.
   */
  private transient ServletContext servletContext;
  /**
   * FilterConfig.
   */
  private transient FilterConfig filterConfig;
  /**
   * The path to the folder, relative to the root, used to compute rewritten image url.
   */
  private String aggregatedFolderPath;

  /**
   * @return {@link WroConfiguration} singleton instance.
   */
  public WroConfiguration getConfig() {
    return config;
  }

  /**
   * DO NOT CALL THIS METHOD UNLESS YOU ARE SURE WHAT YOU ARE DOING.
   *

   * sets the {@link WroConfiguration} singleton instance.
   */
  public void setConfig(final WroConfiguration config) {
    notNull(config);
    this.config = config;
  }

  /**
   * A context useful for running in web context (inside a servlet container).
   */
  public static Context webContext(final HttpServletRequest request, final HttpServletResponse response,
    final FilterConfig filterConfig) {
    return new Context(request, response, filterConfig);
  }

  /**
   * A context useful for running in non web context (standalone applications).
   */
  public static Context standaloneContext() {
    return new Context();
  }

  /**
   * @return {@link Context} associated with CURRENT request cycle.
   */
  public static Context get() {
    validateContext();
    final String correlationId = CORRELATION_ID.get();
    return CONTEXT_MAP.get(correlationId);
  }

  /**
   * @return true if the call is done during wro4j request cycle. In other words, if the context is set.
   */
  public static boolean isContextSet() {
    return CORRELATION_ID.get() != null && CONTEXT_MAP.get(CORRELATION_ID.get()) != null;
  }

  /**
   * Checks if the {@link Context} is accessible from current request cycle.
   */
  private static void validateContext() {
    if (!isContextSet()) {
      throw new WroRuntimeException("No context associated with CURRENT request cycle!");
    }
  }

  /**
   * Set a context with default configuration to current thread.
   */
  public static void set(final Context context) {
    set(context, new WroConfiguration());
  }

  /**
   * Associate a context with the CURRENT request cycle.
   *
   * @param context {@link Context} to set.
   */
  public static void set(final Context context, final WroConfiguration config) {
    notNull(context);
    notNull(config);
    context.setConfig(config);

    final String correlationId = generateCorrelationId();
    CORRELATION_ID.set(correlationId);
    CONTEXT_MAP.put(correlationId, context);
  }

  /**
   * @return a string representation of an unique id used to store Context in a map.
   */
  private static String generateCorrelationId() {
    return UUID.randomUUID().toString();
  }

  /**
   * Remove context from the local thread.
   */
  public static void unset() {
    final String correlationId = CORRELATION_ID.get();
    if (correlationId != null) {
      CONTEXT_MAP.remove(correlationId);
    }
    CORRELATION_ID.remove();
  }

  /**
   * Perform context clean-up.
   */
  public static void destroy() {
    unset();
    //remove all context objects stored in map
    CONTEXT_MAP.clear();
  }

  /**
   * Set the correlationId to the current thread.
   */
  public static void setCorrelationId(final String correlationId) {
    notNull(correlationId);
    CORRELATION_ID.set(correlationId);
  }

  /**
   * Remove the correlationId from the current thread. This operation will not remove the {@link Context} associated
   * with the correlationId. In order to remove context, call {@link Context#unset()}.
   *

   * Unsetting correlationId is useful when you create child threads which needs to access the correlationId from the
   * parent thread. This simulates the {@link InheritableThreadLocal} functionality.
   */
  public static void unsetCorrelationId() {
    CORRELATION_ID.remove();
  }

  /**
   * @return the correlationId associated with this thread.
   */
  public static String getCorrelationId() {
    validateContext();
    return CORRELATION_ID.get();
  }

  /**
   * Decorates a callable with {@link ContextPropagatingCallable} making it possible to access the {@link Context} from
   * within the decorated callable.
   *
   * @param callable
   *          the {@link Callable} to decorate.
   * @return the decorated callable.
   */
  public static <T> Callable<T> decorate(final Callable<T> callable) {
    return new ContextPropagatingCallable<T>(callable);
  }

  /**
   * @return number of {@link Context} objects which were created but not yet destroyed. This is useful to detect leaks
   *         (mostly for unit testing).
   */
  public static int countActive() {
    return CONTEXT_MAP.size();
  }

  /**
   * Private constructor. Used to build {@link StandAloneContext}.
   */
  private Context() {}

  /**
   * Constructor.
   */
  private Context(final HttpServletRequest request, final HttpServletResponse response, final FilterConfig filterConfig) {
    notNull(request);
    notNull(response);
    this.request = request;
    this.response = response;
    this.servletContext = filterConfig != null ? filterConfig.getServletContext() : null;
    this.filterConfig = filterConfig;
  }

  /**
   * @return the request
   */
  public HttpServletRequest getRequest() {
    return this.request;
  }

  /**
   * @return the response
   */
  public HttpServletResponse getResponse() {
    return this.response;
  }

  /**
   * @return the servletContext
   */
  public ServletContext getServletContext() {
    return this.servletContext;
  }

  /**
   * @return the filterConfig
   */
  public FilterConfig getFilterConfig() {
    return this.filterConfig;
  }

  public String getAggregatedFolderPath() {
    return this.aggregatedFolderPath;
  }

  /**
   * @param aggregatedFolderPath the aggregatedFolderPath to set
   */
  public void setAggregatedFolderPath(final String aggregatedFolderPath) {
    this.aggregatedFolderPath = aggregatedFolderPath;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
  }
}


package ro.isdc.wro.cache.support;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.cache.CacheKey;
import ro.isdc.wro.cache.CacheStrategy;
import ro.isdc.wro.cache.CacheValue;
import ro.isdc.wro.config.ReadOnlyContext;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.group.processor.GroupsProcessor;
import ro.isdc.wro.model.resource.support.MutableResourceAuthorizationManager;
import ro.isdc.wro.model.resource.support.ResourceAuthorizationManager;
import ro.isdc.wro.model.resource.support.change.ResourceWatcher;
import ro.isdc.wro.model.resource.support.hash.HashStrategy;
import ro.isdc.wro.util.LazyInitializer;
import ro.isdc.wro.util.SchedulerHelper;

/**
 * Responsible for invoking {@link GroupsProcessor} when cache key is missed.
 *
 * @author Alex Objelean
 * @crated 2 May 2012
 * @since 1.4.6
 */
public class DefaultSynchronizedCacheStrategyDecorator
    extends AbstractSynchronizedCacheStrategyDecorator<CacheKey, CacheValue> {
  private static final Logger LOG = LoggerFactory.getLogger(DefaultSynchronizedCacheStrategyDecorator.class);
  @Inject
  private GroupsProcessor groupsProcessor;
  @Inject
  private HashStrategy hashStrategy;
  @Inject
  private ResourceAuthorizationManager authorizationManager;
  @Inject
  private ReadOnlyContext context;
  @Inject
  private ResourceWatcher resourceWatcher;

  /**
   * Holds the keys that were checked for change. As long as a key is contained in this set, it won't be checked again.
   */
  private final Set<CacheKey> checkedKeys = Collections.synchronizedSet(new HashSet<CacheKey>());
  private final SchedulerHelper resourceWatcherScheduler;

  /**
   * Decorates the provided {@link CacheStrategy}. The provided {@link CacheStrategy} won't be decorated if the
   * operation is redundant.
   */
  public static CacheStrategy<CacheKey, CacheValue> decorate(final CacheStrategy<CacheKey, CacheValue> decorated) {
    return decorated instanceof DefaultSynchronizedCacheStrategyDecorator ? decorated
        : new DefaultSynchronizedCacheStrategyDecorator(decorated);
  }

  /**
   * Based on provided {@link CacheKey} a new key is created which has the same value. This is useful to avoid hashCode
   * variation for minimize flag. This does make sense for resource watcher functionality, when the changes for original
   * resources are performed.
   */
  private static CacheKey createIgnoreMinimizeFlagKey(final CacheKey cacheKey) {
    return new CacheKey(cacheKey.getGroupName(), cacheKey.getType());
  }

  /**
   * @VisibleForTesting
   */
  DefaultSynchronizedCacheStrategyDecorator(final CacheStrategy<CacheKey, CacheValue> cacheStrategy) {
    super(cacheStrategy);
    resourceWatcherScheduler = newResourceWatcherScheduler();
  }

  /**
   * @VisibleForTesting
   */
  SchedulerHelper newResourceWatcherScheduler() {
    return SchedulerHelper.create(new LazyInitializer<Runnable>() {
      @Override
      protected Runnable initialize() {
        return new Runnable() {
          public void run() {
            checkedKeys.clear();
          }
        };
      }
    }, "resourceWatcherScheduler");
  }

  @Override
  protected CacheValue loadValue(final CacheKey key) {
    resourceWatcherScheduler.scheduleWithPeriod(getResourceWatcherUpdatePeriod(), getTimeUnitForResourceWatcher());
    LOG.debug("load value in cache for key: {}", key);
    final String content = groupsProcessor.process(key);
    if (LOG.isDebugEnabled()) {
      LOG.debug("found content: {}", StringUtils.abbreviate(content, 30));
    }
    return computeCacheValueByContent(content);
  }

  private long getResourceWatcherUpdatePeriod() {
    return context.getConfig().getResourceWatcherUpdatePeriod();
  }

  /**
   * @return {@link TimeUnit} used to run resourceWatcher.
   * @VisibleForTesting
   */
  TimeUnit getTimeUnitForResourceWatcher() {
    return TimeUnit.SECONDS;
  }

  /**
   * Creates a {@link CacheValue} based on provided content.
   */
  private CacheValue computeCacheValueByContent(final String content) {
    String hash = null;
    try {
      if (content != null) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Content to fingerprint: [{}]", StringUtils.abbreviate(content, 30));
        }
        hash = hashStrategy.getHash(new ByteArrayInputStream(content.getBytes()));
      }
      final CacheValue entry = CacheValue.valueOf(content, hash);
      LOG.debug("computed entry: {}", entry);
      return entry;
    } catch (final IOException e) {
      throw new RuntimeException("Should never happen", e);
    }
  }

  @Override
  protected void onBeforeGet(final CacheKey key) {
    if (shouldWatchForChange(key)) {
      LOG.debug("tryAsyncCheck");
      if (resourceWatcher.tryAsyncCheck(key)) {
        checkedKeys.add(createIgnoreMinimizeFlagKey(key));
      }
    }
  }

  /**
   * @return true if the provided key should be checked for change.
   */
  private boolean shouldWatchForChange(final CacheKey key) {
    final boolean result = getResourceWatcherUpdatePeriod() > 0 && !wasCheckedForChange(key);
    LOG.debug("shouldWatchForChange={}", result);
    return result;
  }

  @Override
  public void clear() {
    super.clear();
    // reset authorization manager (clear any stored uri's).
    if (authorizationManager instanceof MutableResourceAuthorizationManager) {
      ((MutableResourceAuthorizationManager) authorizationManager).clear();
    }
  }

  @Override
  public void destroy() {
    super.destroy();
    resourceWatcherScheduler.destroy();
  }

  /**
   * @return true if the provided key was checked for change since last resourceWatcher update period iteration started.
   * @VisibleForTesting
   */
  boolean wasCheckedForChange(final CacheKey key) {
    return checkedKeys.contains(key);
  }
}


package ro.isdc.wro.model.resource.support.change;

import static org.apache.commons.lang3.Validate.notNull;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.cache.CacheKey;
import ro.isdc.wro.cache.CacheStrategy;
import ro.isdc.wro.cache.CacheValue;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.config.ReadOnlyContext;
import ro.isdc.wro.config.support.ContextPropagatingCallable;
import ro.isdc.wro.http.WroFilter;
import ro.isdc.wro.http.handler.ResourceWatcherRequestHandler;
import ro.isdc.wro.http.support.PreserveDetailsRequestWrapper;
import ro.isdc.wro.manager.callback.LifecycleCallbackRegistry;
import ro.isdc.wro.model.WroModelInspector;
import ro.isdc.wro.model.factory.WroModelFactory;
import ro.isdc.wro.model.group.Group;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.group.processor.Injector;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.locator.support.DispatcherStreamLocator;
import ro.isdc.wro.model.resource.processor.Destroyable;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.model.resource.processor.decorator.ExceptionHandlingProcessorDecorator;
import ro.isdc.wro.model.resource.processor.impl.css.AbstractCssImportPreProcessor;
import ro.isdc.wro.model.resource.processor.impl.css.CssImportPreProcessor;
import ro.isdc.wro.util.DestroyableLazyInitializer;
import ro.isdc.wro.util.StopWatch;
import ro.isdc.wro.util.WroUtil;

/**
 * A runnable responsible for watching if any resources were changed and invalidate the cache entry for the group
 * containing obsolete resources. This class is thread-safe.
 *
 * @author Alex Objelean
 * @created 06 Aug 2012
 * @since 1.4.8
 */
public class ResourceWatcher
    implements Destroyable {
  private static final Logger LOG = LoggerFactory.getLogger(ResourceWatcher.class);
  /**
   * The thread pool size of the executor which is responsible for performing async check
   */
  private static final int POOL_SIZE = Runtime.getRuntime().availableProcessors();

  public static interface Callback {
    /**
     * Callback method invoked when a group change is detected.
     *
     * @param key
     *          {@link CacheKey} associated with the group whose change was detected.
     */
    void onGroupChanged(final CacheKey key);

    /**
     * Invoked when the change of the resource is detected.
     *
     * @param resource
     *          the {@link Resource} which changed.
     */
    void onResourceChanged(final Resource resource);
  }

  /**
   * Default implementation of {@link Callback} which does nothing by default.
   */
  public static class CallbackSupport
      implements Callback {
    public void onGroupChanged(final CacheKey key) {
    }

    public void onResourceChanged(final Resource resource) {
    };
  }

  @Inject
  private WroModelFactory modelFactory;
  @Inject
  private UriLocatorFactory locatorFactory;
  @Inject
  private Injector injector;
  @Inject
  private LifecycleCallbackRegistry lifecycleCallback;
  @Inject
  private ResourceChangeDetector resourceChangeDetector;
  @Inject
  private CacheStrategy<CacheKey, CacheValue> cacheStrategy;
  @Inject
  private ReadOnlyContext context;
  @Inject
  private DispatcherStreamLocator dispatcherLocator;
  /**
   * Executor responsible for running the check asynchronously.
   */
  private final DestroyableLazyInitializer<ExecutorService> executorServiceRef = new DestroyableLazyInitializer<ExecutorService>() {
    @Override
    protected ExecutorService initialize() {
      return Executors.newFixedThreadPool(POOL_SIZE, WroUtil.createDaemonThreadFactory(ResourceWatcher.class.getName()));
    }

    @Override
    public void destroy() {
      if (isInitialized()) {
        get().shutdownNow();
      }
      super.destroy();
    };
  };

  /**
   * Default constructor with a NoOP callback.
   */
  public void check(final CacheKey cacheKey) {
    check(cacheKey, new CallbackSupport());
  }

  /**
   * Will try an asynchronous check if the async configuration is enabled. If async check is not configured, a
   * synchronous check will be performed. The async check assumes that the {@link ResourceWatcherRequestHandler} is
   * enabled.
   * <p/>
   * If the async check is not allowed (the request was not passed through {@link WroFilter}) - no check will be
   * performed. This is important for use-cases when wro resource is included using a taglib which performs a wro api
   * call directly, without being invoked through {@link WroFilter}.
   *
   * @return true if the actual check invocation was performed. This is important to decide if the resource change
   *         should be skipped or not.
   */
  public boolean tryAsyncCheck(final CacheKey cacheKey) {
    boolean checkInvoked = false;
    if (context.getConfig().isResourceWatcherAsync()) {
      if (isAsyncCheckAllowed()) {
        LOG.debug("Checking resourceWatcher asynchronously...");
        final Callable<Void> callable = createAsyncCheckCallable(cacheKey);
        submit(callable);
        checkInvoked = true;
      }
    } else {
      LOG.debug("Async check not allowed. Falling back to sync check.");
      check(cacheKey);
      checkInvoked = true;
    }
    return checkInvoked;
  }

  /**
   * @return true only if the async is enabled by configuration and if the original request was for a wro resource
   *         (passed through {@link WroFilter}).
   */
  private boolean isAsyncCheckAllowed() {
    return WroFilter.isPassedThroughyWroFilter(Context.get().getRequest());
  }

  /**
   * @VisibleForTesting
   * @param callable
   *          {@link Callable} to submit for asynchronous execution.
   */
  void submit(final Callable<Void> callable) {
    executorServiceRef.get().submit(callable);
  }

  /**
   * Check if resources from a group were changed. If a change is detected, the changeListener will be invoked.
   *
   * @param cacheKey
   *          the cache key which was requested. The key contains the groupName which has to be checked for changes.
   */
  public void check(final CacheKey cacheKey, final Callback callback) {
    notNull(cacheKey);
    LOG.debug("started");
    final StopWatch watch = new StopWatch();
    watch.start("detect changes");
    try {
      final Group group = new WroModelInspector(modelFactory.create()).getGroupByName(cacheKey.getGroupName());
      if (isGroupChanged(group.collectResourcesOfType(cacheKey.getType()), callback)) {
        callback.onGroupChanged(cacheKey);
        cacheStrategy.put(cacheKey, null);
      }
      resourceChangeDetector.reset();
    } catch (final Exception e) {
      onException(e);
    } finally {
      watch.stop();
      LOG.debug("resource watcher info: {}", watch.prettyPrint());
    }
  }

  /**
   * Invoked when exception occurs.
   */
  protected void onException(final Exception e) {
    // not using ERROR log intentionally, since this error is not that important
    LOG.info("Could not check for resource changes because: {}", e.getMessage());
    LOG.debug("[FAIL] detecting resource change ", e);
  }

  private boolean isGroupChanged(final Group group, final Callback callback) {
    final List<Resource> resources = group.getResources();
    final AtomicBoolean isChanged = new AtomicBoolean(false);
    final List<Future<?>> futures = new ArrayList<Future<?>>();
    final boolean isAsync = context.getConfig().isResourceWatcherAsync();
    try {
      for (final Resource resource : resources) {
        if (isAsync) {
          futures.add(executorServiceRef.get().submit(ContextPropagatingCallable.decorate(new Callable<Void>() {
            public Void call()
                throws Exception {
              checkResourceChange(resource, group, callback, isChanged);
              return null;
            }
          })));
        } else {
          checkResourceChange(resource, group, callback, isChanged);
        }
      }
      if (isAsync) {
        LOG.debug("await async execution");
        //await for all futures to complete before returning the result
        for (final Future<?> future : futures) {
          future.get();
        }
      }
    } catch (final Exception e) {
      LOG.debug("Exception while onResourceChange is invoked", e);
    }
    LOG.debug("group={}, changed={}", group.getName(), isChanged);
    return isChanged.get();
  }

  /**
   * Will check if a given resource was changed and will invoke the appropriate callback.
   */
  private void checkResourceChange(final Resource resource, final Group group, final Callback callback,
      final AtomicBoolean isChanged)
      throws Exception {
    if (isChanged(resource, group.getName())) {
      isChanged.compareAndSet(false, true);
      callback.onResourceChanged(resource);
      lifecycleCallback.onResourceChanged(resource);
    }
  }

  /**
   * Check if the resource was changed from previous run. The implementation uses resource content digest (hash) to
   * check for change.
   *
   * @param resource
   *          the {@link Resource} to check.
   * @return true if the resource was changed.
   */
  private boolean isChanged(final Resource resource, final String groupName) {
    boolean changed = false;
    try {
      final String uri = resource.getUri();
      // using AtomicBoolean because we need to mutate this variable inside an anonymous class.
      final AtomicBoolean changeDetected = new AtomicBoolean(resourceChangeDetector.checkChangeForGroup(uri, groupName));
      if (!changeDetected.get() && resource.getType() == ResourceType.CSS) {
        final Reader reader = new InputStreamReader(locatorFactory.locate(uri));
        LOG.debug("\tCheck @import directive from {}", resource);
        createCssImportProcessor(changeDetected, groupName).process(resource, reader, new StringWriter());
      }
      changed = changeDetected.get();
    } catch (final IOException e) {
      LOG.debug("[FAIL] Cannot check {} resource (Exception message: {}). Assuming it is unchanged...", resource,
          e.getMessage());
    }
    LOG.debug("resource={}, changed={}", resource.getUri(), changed);
    return changed;
  }

  /**
   * @param changeDetected
   *          - flag indicating if the change is detected. When this value is true, the processing will be interrupted
   *          by throwing a {@link RuntimeException}.
   * @param groupName
   *          the name of the group being processed.
   * @return a processor used to detect changes in imported resources.
   */
  private ResourcePreProcessor createCssImportProcessor(final AtomicBoolean changeDetected, final String groupName) {
    final ResourcePreProcessor cssImportProcessor = new AbstractCssImportPreProcessor() {
      @Override
      protected void onImportDetected(final String importedUri) {
        LOG.debug("Found @import {}", importedUri);
        final boolean isImportChanged = isChanged(Resource.create(importedUri, ResourceType.CSS), groupName);
        LOG.debug("\tisImportChanged={}", isImportChanged);
        if (isImportChanged) {
          changeDetected.set(true);
          // we need to continue in order to store the hash for all imported resources, otherwise the change won't be
          // computed correctly.
        }
      };

      @Override
      protected String doTransform(final String cssContent, final List<Resource> foundImports)
          throws IOException {
        // no need to build the content, since we are interested in finding imported resources only
        return "";
      }

      @Override
      public String toString() {
        return CssImportPreProcessor.class.getSimpleName();
      }
    };
    /**
     * Ignore processor failure, since we are interesting in detecting change only. A failure is treated as lack of
     * change.
     */
    final ResourcePreProcessor processor = new ExceptionHandlingProcessorDecorator(cssImportProcessor) {
      @Override
      protected boolean isIgnoreFailingProcessor() {
        return true;
      }
    };
    injector.inject(processor);
    return processor;
  }

  private Callable<Void> createAsyncCheckCallable(final CacheKey cacheKey) {
    final HttpServletRequest originalRequest = Context.get().getRequest();
    LOG.debug("OriginalRequest: url={}, uri={}, servletPath={}", originalRequest.getRequestURL(),
        originalRequest.getRequestURI(), originalRequest.getServletPath());
    final HttpServletRequest request = new PreserveDetailsRequestWrapper(originalRequest);
    return ContextPropagatingCallable.decorate(new Callable<Void>() {
      public Void call()
          throws Exception {
        final String location = ResourceWatcherRequestHandler.createHandlerRequestPath(cacheKey, request);
        try {
          dispatcherLocator.locateExternal(request, location);
          return null;
        } catch (final IOException e) {
          final StringBuffer message = new StringBuffer("Could not check the following cacheKey: " + cacheKey);
          if (e instanceof SocketTimeoutException) {
            message.append(". The invocation of ").append(location).append(
                " timed out. Consider increasing the connectionTimeout configuration.");
            LOG.error(message.toString());
          } else {
            LOG.error(message.toString(), e);
          }
          throw e;
        }
      }
    });
  }

  /**
   * @VisibleForTesting
   */
  ResourceChangeDetector getResourceChangeDetector() {
    return resourceChangeDetector;
  }

  public void destroy()
      throws Exception {
    executorServiceRef.destroy();
  }
}


package ro.isdc.wro.model.resource.support.change;

import static org.apache.commons.lang3.Validate.notNull;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Holds details about hashes of watched resources and the group which were detected as changed.
 */
public final class ResourceChangeInfo {
  private static final Logger LOG = LoggerFactory.getLogger(ResourceChangeInfo.class);
  /**
   * The hash of the resource retrieving during current request cycle.
   */
  private String currentHash;
  /**
   * Persisted hash of the resource retrieved during previous request cycle.
   */
  private String prevHash;
  /**
   * A set of groups for which changes were detected. This set is required to handle the situations when the same
   * resource is contained in two different groups and only the first group is notified about the change.
   */
  private final Set<String> groups = Collections.synchronizedSet(new HashSet<String>());

  /**
   * Updates the hash associated with the resource for a give groupName.
   *
   * @param hash
   *          the most recent computed hash.
   * @param groupName
   *          name of the group for which the change of resource is detected.
   */
  public void updateHashForGroup(final String hash, final String groupName) {
    notNull(groupName);
    this.currentHash = hash;
    if (isChangedHash()) {
      LOG.debug("Group {} has changed", groupName);
      //remove all persisted groups. Starting over..
      groups.clear();
    }
  }

  private boolean isChangedHash() {
    return currentHash != null ? !currentHash.equals(prevHash) : prevHash != null;
  }

  /**
   * Resets the state of this {@link ResourceChangeInfo} by copying the current hash to previous hash and reseting
   * current hash to null. This operation should be invoked after a change cycle completes and a new one is prepared.
   */
  public void reset() {
    // This is important to avoid false positives when reset is called concurrently.
    if (currentHash != null) {
      this.prevHash = currentHash;
    }
    this.currentHash = null;
  }

  /**
   * @param groupName
   *          associated with the current change check.
   * @return true if the change is detected for provided group.
   */
  public boolean isChanged(final String groupName) {
    notNull(groupName);
    final boolean result = isChangedHash() ? true : !groups.contains(groupName);
    if (result) {
      groups.add(groupName);
    }
    return result;
  }

  /**
   * Checks if the current hash was set previously and persist the groupName in the set of the groups who are interested
   * about the change.
   *
   * @param groupName
   *          the name of the group which contains the resource for which check is performed.
   * @return true if the change is required.
   */
  public boolean isCheckRequiredForGroup(final String groupName) {
    notNull(groupName);
    return currentHash == null;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this);
  }
}


/*
 * Copyright (c) 2008. All rights reserved.
 */
package ro.isdc.wro.model.group;

import static org.apache.commons.lang3.Validate.notNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;

/**
 * A group is an entity holding a list of resources.
 *

 * This class is thread safe.
 *
 * @author Alex Objelean
 * @created Created on Oct 30, 2008
 */
public final class Group implements Comparable<Group> {
  private static final Logger LOG = LoggerFactory.getLogger(Group.class);
  /**
   * Group name.
   */
  private String name;

  /**
   * Resources of the group.
   */
  private final List<Resource> resources = Collections.synchronizedList(new ArrayList<Resource>());

  /**
   * To be used by JSON serializer.
   */
  public Group() {
  }

  /**
   * Creates a group with a name.
   *
   * @param name
   *          of the group.
   */
  public Group(final String name) {
    notNull(name, "Group name cannot be null.");
    this.name = name;
  }

  /**
   * Check if the group has at least one resource of some type.
   *
   * @param resourceType
   *          type of the searched resource.
   * @return true if at least one resource of some type exists.
   */
  public final boolean hasResourcesOfType(final ResourceType resourceType) {
    notNull(resourceType, "ResourceType cannot be null!");
    for (final Resource resource : resources) {
      if (resourceType.equals(resource.getType())) {
        return true;
      }
    }
    return false;
  }

  /**
   * @return true if the resourceToCheck is already contained in this group.
   */
  private boolean hasResource(final Resource resourceToCheck) {
    for (final Resource resource : resources) {
      if (resource.equals(resourceToCheck)) {
        return true;
      }
    }
    return false;
  }

  /**
   * @return true if a {@link Resource} with an uri same as resourceUri is contained in this group.
   */
  public boolean hasResource(final String resourceUri) {
    for (final Resource resource : resources) {
      if (resource.getUri().equals(resourceUri)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Replace one resource with a list of other resources. The use case is related to wildcard expander functionality,
   * when resources containing wildcard are replaced with a list of wildcard-free resources. The order of resources is
   * preserved.
   *

   * The implementation is synchronized, because it mutates the collection.
   *
   * @param resource
   *          to replace.
   * @param expandedResources
   *          a list of resources to replace. If this list is empty, the result is similar to removing the resource from
   *          the group.
   * @throws IllegalArgumentException
   *           when a missing resources is to be replaced.
   */
  public void replace(final Resource resource, final List<Resource> expandedResources) {
    LOG.debug("replacing resource {} with expanded resources: {}", resource, expandedResources);
    notNull(resource);
    notNull(expandedResources);
    synchronized (this) {
      boolean found = false;
      // use set to avoid duplicates
      final Set<Resource> result = new LinkedHashSet<Resource>();
      for (final Resource resourceItem : resources) {
        if (resourceItem.equals(resource)) {
          found = true;
          for (final Resource expandedResource : expandedResources) {
            // preserve minimize flag.
            expandedResource.setMinimize(resource.isMinimize());
            result.add(expandedResource);
          }
        } else {
          result.add(resourceItem);
        }
      }
      // if no resources found, an invalid replace is performed
      if (!found) {
        throw new IllegalArgumentException("Cannot replace resource: " + resource + " for group: " + this
            + " because the resource is not a part of this group.");
      }
      // update resources with newly built list.
      setResources(new ArrayList<Resource>(result));
    }
  }

  /**
   * @param type
   *          of resources to collect. This value should not be null.
   * @return a new group with the same name as original containing filtered resources. The created group has the same
   *         name.
   */
  public final Group collectResourcesOfType(final ResourceType type) {
    notNull(type);
    final List<Resource> allResources = new ArrayList<Resource>();
    allResources.addAll(getResources());

    // retain only resources of needed type
    final List<Resource> filteredResources = new ArrayList<Resource>();
    for (final Resource resource : getResources()) {
      if (type == resource.getType()) {
        if (filteredResources.contains(resource)) {
          LOG.warn("Duplicated resource detected: " + resource + ". This resource won't be included more than once!");
        } else {
          filteredResources.add(resource);
        }
      }
    }

    final Group filteredGroup = new Group(getName());
    filteredGroup.setResources(filteredResources);
    return filteredGroup;
  }

  /**
   * @return the readonly list of resources.
   */
  public List<Resource> getResources() {
    // use a new list to avoid ConcurrentModificationException when the Group#replace method is called.
    return Collections.unmodifiableList(new ArrayList<Resource>(resources));
  }

  /**
   * Add a {@link Resource} to the collection of resources associated with this group.
   *

   * The implementation is synchronized, because it mutates the collection.
   *
   * @param resource
   *          {@link Resource} to add to this group (at the end).
   * @return the reference to Group (fluent interface).
   */
  public Group addResource(final Resource resource) {
    notNull(resource);
    synchronized (this) {
      if (!hasResource(resource)) {
        resources.add(resource);
      } else {
        LOG.debug("Resource {} is already contained in this group, skipping it.", resource);
      }
    }
    return this;
  }

  /**
   * This method will replace all earlier defined resources with the provided list of resources.
   *

   * The implementation is synchronized, because it mutates the collection.
   *
   * @param resources
   *          the resources to replace the underlying resources.
   */
  public final void setResources(final List<Resource> resources) {
    notNull(resources);
    synchronized (this) {
      this.resources.clear();
      for (final Resource resource : resources) {
        addResource(resource);
      }
    }
  }

  /**
   * @return the name
   */
  public String getName() {
    return name;
  }

  public int compareTo(final Group o) {
    return getName().compareTo(o.getName());
  }

  @Override
  public boolean equals(final Object obj) {
    // Because name is unique, we can consider two groups are equals if their name is the same
    if (obj instanceof Group) {
      final Group group = (Group) obj;
      return getName().equals(group.getName());
    }
    return false;
  }

  @Override
  public int hashCode() {
    return getName().hashCode();
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
  }
}


package ro.isdc.wro.model.resource.support;

import static org.apache.commons.lang3.Validate.notNull;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.util.provider.ProviderFinder;

/**
 * Abstracts the configurable creation of the strategies based on {@link ProviderFinder}. Uses two generic parameters:
 *
 * @param <S>
 *          strategy type
 * @param

 *          provider type
 * @author Alex Objelean
 * @created 26 Jun 2012
 * @since 1.4.7
 */
public abstract class AbstractConfigurableStrategySupport<S, P> {
  private static final Logger LOG = LoggerFactory.getLogger(AbstractConfigurableStrategySupport.class);

  private ProviderFinder
 providerFinder;
  private Map<String, S> map;
  private Properties properties;

  /**
   * Override this method if a fallback search is required.
   *
   * @param alias
   *          of the strategy to use.
   * @return the strategy S if one is found or null if no corresponding strategy found.
   */
  protected S getStrategyForAlias(final String alias) {
    return getStrategyMap().get(alias);
  }

  /**
   * @return the value configured in the properties file using the strategy key.
   */
  protected final String getConfiguredValue() {
    return getProperties().getProperty(getStrategyKey());
  }

  /**
   * @return a set of available aliases.
   */
  public final Set<String> getAvailableAliases() {
    return getStrategyMap().keySet();
  }

  /**
   * @return a set of available strategies.
   * @VisibleForTesting
   */
  public final Collection<S> getAvailableStrategies() {
    return getStrategyMap().values();
  }

  /**
   * @return the map where the strategy alias will be searched. By default a {@link ProviderFinder} is used to build the
   *         map. If two providers have same alias, the one with higher order will override those with lower order.
   */
  private final Map<String, S> newStrategyMap() {
    //preserve the order
    final Map<String, S> map = new LinkedHashMap<String, S>();
    final List
 providers = getProviderFinder().find();
    //reverse before adding to map, to allow highest ordered override lowest one (assuming same alias is used).
    Collections.reverse(providers);
    LOG.debug("providers: {}", providers);
    for (final P provider : providers) {
      try {
        map.putAll(getStrategies(provider));
      } catch (final Exception e) {
        LOG.warn("Could not load strategies for provider: {}, because of: {}. It will be skipped.", provider,
            e.getMessage());
      }
    }
    // allow client to override defaults
    overrideDefaultStrategyMap(map);
    return map;
  }

  /**
   * Invoked after the the map of strategies is built from providers. Allows client to override its keys or add new
   * entries.
   *
   * @param map
   *          the built map.
   */
  protected void overrideDefaultStrategyMap(final Map<String, S> map) {
  }

  /**
   * Utility method which copies all entries from source into target. Entries with the same keys from target will be
   * overridden with entries from source. This operation is similar to {@link Map#putAll(Map)}, but it doesn't require
   * changing generics to construction like
   *
   *

   * <? extends String, ? extends S>
   *

   *
   * @param source
   *          the map from where the entries will be copied into target.
   * @param target
   *          the map where to put entries from source.
   */
  protected final void copyAll(final Map<String, S> source, final Map<String, S> target) {
    notNull(source);
    notNull(target);
    for (final Map.Entry<String, S> entry : source.entrySet()) {
      target.put(entry.getKey(), entry.getValue());
    }
  }

  private Map<String, S> getStrategyMap() {
    if (map == null) {
      map = newStrategyMap();
    }
    return map;
  }

  /**
   * To be used for internal usage. Ensure that returned object is not null.
   */
  private Properties getProperties() {
    if (this.properties == null) {
      this.properties = newProperties();
    }
    return this.properties;
  }

  /**
   * @return {@link Properties} used to lookup namingStrategy alias.
   */
  protected Properties newProperties() {
    return new Properties();
  }

  /**
   * @param props
   *          {@link Properties} containing configured alias.
   */
  public final void setProperties(final Properties props) {
    Validate.notNull(props);
    this.properties = props;
  }

  /**
   * @return the {@link ProviderFinder} used to find all strategies.
   * @VisibleForTesting This method is not final only for testing purposes.
   */
  protected ProviderFinder
 getProviderFinder() {
    if (providerFinder == null) {
      providerFinder = ProviderFinder.of(getProviderClass());
    }
    return providerFinder;
  }

  /**
   * @return the class of the provider of type P. Uses {@link ParameterizedType} to compute the class. Override it to
   *         support anonymous classes which do not play well with {@link ParameterizedType}'s.
   */
  @SuppressWarnings("unchecked")
  protected Class
 getProviderClass() {
    final Type type = getClass().getGenericSuperclass();
    return (Class
) ((ParameterizedType) type).getActualTypeArguments()[1];
  }

  /**
   * @param provider
   *          the instance responsible for strategy lookup.
   * @return the map of provided strategies.
   */
  protected abstract Map<String, S> getStrategies(final P provider);

  /**
   * @return the key of the strategy property.
   */
  protected abstract String getStrategyKey();
}


/*
 * Copyright (C) 2010. All rights reserved.
 */
package ro.isdc.wro.extensions.processor.js;

import static org.apache.commons.lang3.Validate.notNull;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.text.MessageFormat;
import java.util.Arrays;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.AutoCloseInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.SupportedResourceType;
import ro.isdc.wro.model.resource.processor.ResourcePostProcessor;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.model.resource.processor.SupportAware;
import ro.isdc.wro.util.WroUtil;

/**
 * Important node: this processor is not cross platform and has some pre-requesites in order to work.
 *

 * Installation instructions: Install the typescript node package module (Unix OS)
 *
 *

 *    npm install -g typescript
 *

 *
 * It is possible to test whether the tsc utility is available using {@link NodeTypeScriptProcessor#isSupported()}
 *
 * @author Alex Objelean
 * @since 1.5.0
 * @created 10 Sep 2012
 */
@SupportedResourceType(ResourceType.JS)
public class NodeTypeScriptProcessor
    implements ResourcePreProcessor, ResourcePostProcessor, SupportAware {
  private static final String TYPESCRIPT_EXTENSION = "ts";
  private static final String SHELL_COMMAND = "tsc";
  private static final String ARG_OUT = "--out";
  private static final Logger LOG = LoggerFactory.getLogger(NodeTypeScriptProcessor.class);
  public static final String ALIAS = "nodeTypeScript";
  /**
   * Flag indicating that we are running on Windows platform. This will be initialized only once in constructor.
   */
  private final boolean isWindows;

  private static class StreamGobbler
      extends Thread {
    InputStream is;
    String type;

    public StreamGobbler(final InputStream is, final String type) {
      this.is = is;
      this.type = type;
    }

    @Override
    public void run() {
      try {
        final InputStreamReader isr = new InputStreamReader(is);
        final BufferedReader br = new BufferedReader(isr);
        String line = null;
        while ((line = br.readLine()) != null)
          LOG.debug(type + ">" + line);
      } catch (final IOException ioe) {
        ioe.printStackTrace();
      }
    }
  }

  public NodeTypeScriptProcessor() {
    // initialize this field at construction.
    final String osName = System.getProperty("os.name");
    LOG.debug("OS Name: {}", osName);
    isWindows = osName != null && osName.contains("Windows");
  }

  @Override
  public void process(final Resource resource, final Reader reader, final Writer writer)
      throws IOException {
    final String content = IOUtils.toString(reader);
    final String resourceUri = resource == null ? "unknown.js" : resource.getUri();
    try {
      writer.write(process(resourceUri, content));
    } catch (final Exception e) {
      LOG.warn("Exception while applying " + getClass().getSimpleName() + " processor on the " + resourceUri
          + " resource, no processing applied...", e);
      LOG.error(e.getMessage(), e);
      onException(e, content);
    } finally {
      // return for later reuse
      reader.close();
      writer.close();
    }
  }

  private String process(final String resourceUri, final String content) {
    final InputStream shellIn = null;
    OutputStream tempSourceStream = null;
    // the file holding the input file to process
    File tempSource = null;
    File tempDest = null;
    try {
      tempSource = WroUtil.createTempFile(TYPESCRIPT_EXTENSION);
      tempDest = WroUtil.createTempFile(TYPESCRIPT_EXTENSION);
      final String encoding = "UTF-8";
      IOUtils.write(content, tempSourceStream = new FileOutputStream(tempSource), encoding);
      LOG.debug("absolute path: {}", tempSource.getAbsolutePath());

      final Process process = createProcess(tempSource, tempDest);

      final int exitStatus = process.waitFor();// this won't return till `out' stream being flushed!
      final String result = IOUtils.toString(new AutoCloseInputStream(new FileInputStream(tempDest)), encoding);
      if (exitStatus != 0) {
        LOG.error("exitStatus: {}", exitStatus);
        String errorMessage = IOUtils.toString(new AutoCloseInputStream(process.getInputStream()), encoding);
        // find a way to get rid of escape character found at the end (minor issue)
        errorMessage = MessageFormat.format("Error in Typescript: \n{0}",
            errorMessage.replace(tempSource.getPath(), resourceUri));
        throw new WroRuntimeException(errorMessage).logError();
      }
      return result;
    } catch (final Exception e) {
      throw WroRuntimeException.wrap(e);
    } finally {
      // close input stream to allow file to be deleted (otherwise deletion fails).
      IOUtils.closeQuietly(shellIn);
      IOUtils.closeQuietly(tempSourceStream);
      // always cleanUp
      FileUtils.deleteQuietly(tempSource);
      FileUtils.deleteQuietly(tempDest);
    }
  }

  /**
   * Invoked when a processing exception occurs. Default implementation wraps the original exception into
   * {@link WroRuntimeException}.
   *
   * @param e
   *          the {@link Exception} thrown during processing
   * @param content
   *          the resource content being processed.
   */
  protected void onException(final Exception e, final String content) {
    throw WroRuntimeException.wrap(e);
  }

  @Override
  public void process(final Reader reader, final Writer writer)
      throws IOException {
    process(null, reader, writer);
  }

  /**
   * Creates process responsible for running tsc shell command by reading the file content from the sourceFilePath
   *
   * @param sourceFile
   *          the source path of the file from where the tsc will read the typescript file.
   * @param destFile
   *          the destination of the compiled file.
   * @throws IOException
   *           when the process execution fails.
   */
  private Process createProcess(final File sourceFile, final File destFile)
      throws IOException {
    notNull(sourceFile);
    final String[] commandLine = getCommandLine(sourceFile.getPath(), destFile.getPath());
    LOG.debug("CommandLine arguments: {}", Arrays.asList(commandLine));
    final Process process = new ProcessBuilder(commandLine).redirectErrorStream(true).start();

    //Gobblers responsible for reading stream to avoid blocking of the process when the buffer is full.
    final StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR");
    // any output?
    final StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT");
    // kick them off
    errorGobbler.start();
    outputGobbler.start();

    return process;
  }

  /**
   * @return true if the processor is supported on this environment. The implementation check if the required shell
   *         utility is available.
   */
  @Override
  public boolean isSupported() {
    File tempSource = null;
    File tempDest = null;
    try {
      tempSource = WroUtil.createTempFile(TYPESCRIPT_EXTENSION);
      tempDest = WroUtil.createTempFile(TYPESCRIPT_EXTENSION);
      final Process process = createProcess(tempSource, tempDest);
      final int exitValue = process.waitFor();
      LOG.debug("exitValue {}. ErrorMessage: {}", exitValue,
          IOUtils.toString(new AutoCloseInputStream(process.getInputStream())));
      if (exitValue != 0) {
        throw new UnsupportedOperationException("Tsc is not a supported operation on this platform");
      }
      LOG.debug("The {} processor is supported.", getClass().getName());
      return true;
    } catch (final Exception e) {
      LOG.debug("Unsupported processor", e);
      LOG.warn("The {} processor is not supported. Because: {}", getClass().getName(), e.getMessage());
      return false;
    } finally {
      FileUtils.deleteQuietly(tempSource);
      FileUtils.deleteQuietly(tempDest);
    }
  }

  /**
   * Creates the platform specific arguments to run the tsc shell utility. Default implementation handles
   * windows and unix platforms.
   *
   * @return arguments for command line. The implementation will take care of OS differences.
   */
  protected String[] getCommandLine(final String filePath, final String outFilePath) {
    return isWindows ? buildArgumentsForWindows(filePath, outFilePath) : buildArgumentsForUnix(filePath, outFilePath);
  }

  /**
   * @return arguments required to run tsc on non Windows platform.
   */
  private String[] buildArgumentsForUnix(final String filePath, final String outFilePath) {
    return new String[] {
      SHELL_COMMAND, filePath, ARG_OUT, outFilePath
    };
  }

  /**
   * @return arguments required to run tsc on Windows platform.
   */
  private String[] buildArgumentsForWindows(final String filePath, final String outFilePath) {
    return new String[] {
      "cmd", "/c", SHELL_COMMAND, filePath, ARG_OUT, outFilePath
    };
  }
}


/*
 * Copyright wro4j@2011.
 */
package ro.isdc.wro.extensions.processor.support.uglify;

import static ro.isdc.wro.extensions.processor.support.uglify.UglifyJs.Type.UGLIFY;

import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.AutoCloseInputStream;
import org.apache.commons.lang3.Validate;
import org.mozilla.javascript.ScriptableObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.extensions.script.RhinoScriptBuilder;
import ro.isdc.wro.util.StopWatch;
import ro.isdc.wro.util.WroUtil;

/**
 * The underlying implementation use untagged version (commited on 2012-09-08 14:15:12).
 *

 * {@link https://github.com/mishoo/UglifyJS}.
 *

 * The uglify script is resulted from merging of the following two scripts: parse-js.js, process.js.
 *
 * @author Alex Objelean
 * @since 1.3.1
 */
public class UglifyJs {
  private static final Logger LOG = LoggerFactory.getLogger(UglifyJs.class);
  /**
   * The name of the uglify script to be used by default.
   */
  public static final String DEFAULT_UGLIFY_JS = "uglifyJs.min.js";
  private String invokeScript;
  private String defaultOptionsAsJson;
  /**
   * If true, the script is uglified, otherwise it is beautified.
   */
  private final boolean uglify;
  /**
   * Comma delimited variable names to have uglify not mangle
   */
  private String reservedNames;
  private ScriptableObject scope;

  /**
   * The type of processing supported by UglifyJs library. This enum replaces ugly boolean constructor parameter.
   */
  public static enum Type {
    BEAUTIFY, UGLIFY
  }

  /**
   * @return the script responsible for invoking the uglifyJs script.
   */
  private String getInvokeScript()
      throws IOException {
    if (invokeScript == null) {
      invokeScript = IOUtils.toString(new AutoCloseInputStream(UglifyJs.class.getResourceAsStream("invoke.js")));
    }
    return invokeScript;
  }

  /**
   * @param uglify
   *          if true the code will be uglified (compressed and minimized), otherwise it will be beautified (nice
   *          formatted).
   */
  public UglifyJs(final Type uglifyType) {
    Validate.notNull(uglifyType);
    this.uglify = uglifyType == UGLIFY ? true : false;
  }

  /**
   * Factory method for creating the uglifyJs engine.
   */
  public static UglifyJs uglifyJs() {
    return new UglifyJs(UGLIFY);
  }

  /**
   * Factory method for creating the beautifyJs engine.
   */
  public static UglifyJs beautifyJs() {
    return new UglifyJs(Type.BEAUTIFY);
  }

  /**
   * some libraries rely on certain names to be used, so this option allow you to exclude such names from the mangler.
   * For example, to keep names require and $super intact you'd specify –reserved-names "require,$super".
   *
   * @param reservedNames
   *          the reservedNames to set
   */
  public UglifyJs setReservedNames(final String reservedNames) {
    this.reservedNames = reservedNames;
    return this;
  }

  /**
   * @return not null value representing reservedNames.
   */
  private String getReservedNames() {
    return this.reservedNames == null ? "" : reservedNames;
  }

  /**
   * Initialize script builder for evaluation.
   */
  private RhinoScriptBuilder initScriptBuilder() {
    // TODO: Find a way to encapsulate this code
    RhinoScriptBuilder builder = null;
    try {
      if (scope == null) {
        builder = RhinoScriptBuilder.newChain().addJSON().evaluateChain(UglifyJs.class.getResourceAsStream("init.js"),
            "initScript").evaluateChain(getScriptAsStream(), DEFAULT_UGLIFY_JS);
        scope = builder.getScope();
      } else {
        builder = RhinoScriptBuilder.newChain(scope);
      }
      return builder;
    } catch (final Exception ex) {
      throw new IllegalStateException("Failed initializing js", ex);
    }
  }

  /**
   * @return the stream of the uglify script. Override this method to provide a different script version.
   */
  protected InputStream getScriptAsStream() {
    return UglifyJs.class.getResourceAsStream(DEFAULT_UGLIFY_JS);
  }

  /**
   * @param data
   *          js content to process.
   * @return packed js content.
   */
  public String process(final String filename, final String code)
      throws IOException {
    final StopWatch watch = new StopWatch();
    watch.start("init " + filename);
    final RhinoScriptBuilder builder = initScriptBuilder();
    watch.stop();
    final String originalCode = WroUtil.toJSMultiLineString(code);
    // TODO handle reservedNames
    final String optionsAsJson = createOptionsAsJson();
    Validate.notNull(optionsAsJson);
    final String scriptAsString = String.format(getInvokeScript(), originalCode, optionsAsJson);
    watch.start(uglify ? "uglify" : "beautify");
    final Object result = builder.evaluate(scriptAsString, "uglifyIt");

    watch.stop();
    LOG.debug(watch.prettyPrint());
    return String.valueOf(result);
  }

  /**
   * Reads by default options from options.js file located in the same package. This is an example of how the options
   * could look like:
   *
   *

   * {
   *    codegen_options: {
   *      beautify: false,
   *      space_colon: false
   *    },
   *    squeeze: true,
   *    dead_code: true,
   *    mangle: true
   * }
   *

   *
   * @return json representation of options.
   */
  protected String createOptionsAsJson()
      throws IOException {
    return String.format(getDefaultOptions(), !uglify, getReservedNames());
  }

  /**
   * @return default options string representation loaded from options.js resource file.
   */
  private String getDefaultOptions()
      throws IOException {
    if (defaultOptionsAsJson == null) {
      defaultOptionsAsJson = IOUtils.toString(new AutoCloseInputStream(UglifyJs.class.getResourceAsStream("options.js")));
    }
    return defaultOptionsAsJson;
  }
}