content: /**
   * Copyright Alex Objelean
   */
  package ro.isdc.wro.maven.plugin;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Properties;
  import java.util.concurrent.Callable;
  
  import javax.servlet.FilterConfig;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  
  import org.apache.commons.io.IOUtils;
  import org.apache.commons.io.output.ByteArrayOutputStream;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.maven.plugin.MojoExecutionException;
  import org.mockito.Mockito;
  
  import ro.isdc.wro.config.Context;
  import ro.isdc.wro.config.jmx.WroConfiguration;
  import ro.isdc.wro.http.support.DelegatingServletOutputStream;
  import ro.isdc.wro.maven.plugin.support.AggregatedFolderPathResolver;
  import ro.isdc.wro.model.resource.ResourceType;
  import ro.isdc.wro.model.resource.locator.ServletContextUriLocator;
  import ro.isdc.wro.util.StopWatch;
  import ro.isdc.wro.util.io.UnclosableBufferedInputStream;
  
  
  /**
   * A build-time solution for organizing and minimizing static resources. By default uses the same configuration as the
   * run-time solution. Additionally, allows you to change the processors used by changing the wroManagerFactory
   * implementation used by the plugin.
   *
   * @goal run
   * @phase compile
   * @requiresDependencyResolution runtime
   * @author Alex Objelean
   */
  public class Wro4jMojo
      extends AbstractWro4jMojo {
    /**
     * The path to the destination directory where the files are stored at the end of the process.
     *
     * @parameter default-value="${project.build.directory}" property="destinationFolder"
     * @optional
     */
    private File destinationFolder;
    /**
     * @parameter property="cssDestinationFolder"
     * @optional
     */
    private File cssDestinationFolder;
    /**
     * @parameter property="jsDestinationFolder"
     * @optional
     */
    private File jsDestinationFolder;
    /**
     * This parameter is not meant to be used. The only purpose is to hold project build directory
     *
     * @parameter default-value="${project.build.directory}"
     * @optional
     */
    private File buildDirectory;
    /**
     * This parameter is not meant to be used. The only purpose is to hold the final build name of the artifact
     *
     * @parameter default-value="${project.build.directory}/${project.build.finalName}"
     * @optional
     */
    private File buildFinalName;
    /**
     * @parameter property="groupNameMappingFile"
     * @optional
     */
    private File groupNameMappingFile;
    /**
     * Useful when the application is deployed under a contextPath which is different than ROOT (example: "myapp"). This
     * will be used by CssUrlRewritingProcessor to compute properly the url's. By default, the ROOT is assumed, meaning
     * that the rewritten url's will start with "/".
     *
     * @parameter property="contextPath"
     * @optional
     */
    private String contextPath;
    /**
     * Holds a mapping between original group name file & renamed one.
     */
    private final Properties groupNames = new Properties();
  
    @Override
    protected void validate()
        throws MojoExecutionException {
      super.validate();
      // additional validation requirements
      if (destinationFolder == null) {
        throw new MojoExecutionException("destinationFolder was not set!");
      }
    }
  
    @Override
    protected void onBeforeExecute() {
      groupNames.clear();
      if (groupNameMappingFile != null && isIncrementalBuild()) {
        try {
          // reuse stored properties for incremental build
          groupNames.load(new FileInputStream(groupNameMappingFile));
        } catch (final IOException e) {
          getLog().debug("Cannot load " + groupNameMappingFile.getPath());
        }
      }
    }
  
    @Override
    protected void doExecute()
        throws Exception {
      if (contextPath != null) {
        getLog().info("contextPath: " + contextPath);
      }
      getLog().info("destinationFolder: " + destinationFolder);
      if (jsDestinationFolder != null) {
        getLog().info("jsDestinationFolder: " + jsDestinationFolder);
      }
      if (cssDestinationFolder != null) {
        getLog().info("cssDestinationFolder: " + cssDestinationFolder);
      }
      if (groupNameMappingFile != null) {
        getLog().info("groupNameMappingFile: " + groupNameMappingFile);
      }
      final Collection<String> groupsAsList = getTargetGroupsAsList();
      final StopWatch watch = new StopWatch();
      watch.start("processGroups: " + groupsAsList);
  
      final Collection<Callable<Void>> callables = new ArrayList<Callable<Void>>();
  
      for (final String group : groupsAsList) {
        for (final ResourceType resourceType : ResourceType.values()) {
          final File destinationFolder = computeDestinationFolder(resourceType);
          final String groupWithExtension = group + "." + resourceType.name().toLowerCase();
  
          if (isParallelProcessing()) {
            callables.add(Context.decorate(new Callable<Void>() {
              public Void call()
                  throws Exception {
                processGroup(groupWithExtension, destinationFolder);
                return null;
              }
            }));
          } else {
            processGroup(groupWithExtension, destinationFolder);
          }
        }
      }
      if (isParallelProcessing()) {
        getTaskExecutor().submit(callables);
      }
      watch.stop();
      getLog().debug(watch.prettyPrint());
      writeGroupNameMap();
    }
  
    @Override
    protected boolean isIncrementalCheckRequired() {
      return super.isIncrementalCheckRequired() && destinationFolder.exists();
    }
  
    private void writeGroupNameMap()
        throws Exception {
      if (groupNameMappingFile != null) {
        FileOutputStream outputStream = null;
        try {
          final File mappingFileParent = new File(groupNameMappingFile.getParent());
          // create missing folders if needed
          mappingFileParent.mkdirs();
          outputStream = new FileOutputStream(groupNameMappingFile);
          groupNames.store(outputStream, "Mapping of defined group name to renamed group name");
        } catch (final FileNotFoundException ex) {
          throw new MojoExecutionException("Unable to save group name mapping file", ex);
        } finally {
          IOUtils.closeQuietly(outputStream);
        }
      }
    }
  
    /**
     * Encodes a version using some logic.
     *
     * @param group
     *          the name of the resource to encode.
     * @param input
     *          the stream of the result content.
     * @return the name of the resource with the version encoded.
     */
    private String rename(final String group, final InputStream input)
        throws Exception {
      try {
        final String newName = getManagerFactory().create().getNamingStrategy().rename(group, input);
        groupNames.setProperty(group, newName);
        return newName;
      } catch (final IOException e) {
        throw new MojoExecutionException("Error occured during renaming", e);
      }
    }
  
    /**
     * Computes the destination folder based on resource type.
     *
     * @param resourceType
     *          {@link ResourceType} to process.
     * @return destinationFoder where the result of resourceType will be copied.
     * @throws MojoExecutionException
     *           if computed folder is null.
     */
    private File computeDestinationFolder(final ResourceType resourceType)
        throws MojoExecutionException {
      File folder = destinationFolder;
      if (resourceType == ResourceType.JS) {
        if (jsDestinationFolder != null) {
          folder = jsDestinationFolder;
        }
      }
      if (resourceType == ResourceType.CSS) {
        if (cssDestinationFolder != null) {
          folder = cssDestinationFolder;
        }
      }
      getLog().info("folder: " + folder);
      if (folder == null) {
        throw new MojoExecutionException("Couldn't compute destination folder for resourceType: " + resourceType
            + ". That means that you didn't define one of the following parameters: "
            + "destinationFolder, cssDestinationFolder, jsDestinationFolder");
      }
      if (!folder.exists()) {
        folder.mkdirs();
      }
      return folder;
    }
  
    /**
     * Process a single group.
     */
    private void processGroup(final String group, final File parentFoder)
        throws Exception {
      ByteArrayOutputStream resultOutputStream = null;
      InputStream resultInputStream = null;
      try {
        getLog().info("processing group: " + group);
  
        // mock request
        final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
        Mockito.when(request.getContextPath()).thenReturn(normalizeContextPath(contextPath));
        Mockito.when(request.getRequestURI()).thenReturn(group);
        // mock response
        final HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
        resultOutputStream = new ByteArrayOutputStream();
        Mockito.when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(resultOutputStream));
  
        // init context
        final WroConfiguration config = Context.get().getConfig();
        // the maven plugin should ignore empty groups, since it will try to process all types of resources.
        config.setIgnoreEmptyGroup(true);
        Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class)), config);
  
        Context.get().setAggregatedFolderPath(getAggregatedPathResolver().resolve());
        // perform processing
        getManagerFactory().create().process();
        // encode version & write result to file
        resultInputStream = new UnclosableBufferedInputStream(resultOutputStream.toByteArray());
        final File destinationFile = new File(parentFoder, rename(group, resultInputStream));
        final File parentFolder = destinationFile.getParentFile();
        if (!parentFolder.exists()) {
          // make directories if required
          parentFolder.mkdirs();
        }
        destinationFile.createNewFile();
        // allow the same stream to be read again
        resultInputStream.reset();
        getLog().debug("Created file: " + destinationFile.getName());
  
        final OutputStream fos = new FileOutputStream(destinationFile);
        // use reader to detect encoding
        IOUtils.copy(resultInputStream, fos);
        fos.close();
        // delete empty files
        if (destinationFile.length() == 0) {
          getLog().debug("No content found for group: " + group);
          destinationFile.delete();
        } else {
          getLog().info("file size: " + destinationFile.getName() + " -> " + destinationFile.length() + " bytes");
          getLog().info(destinationFile.getAbsolutePath() + " (" + destinationFile.length() + " bytes" + ")");
        }
      } finally {
        if (resultOutputStream != null) {
          resultOutputStream.close();
        }
        if (resultInputStream != null) {
          resultInputStream.close();
        }
      }
    }
  
    /**
     * @return normalized representation of the context path. Example: "/myapp". Will add or strip "/" separator depending
     *         on provided input.
     */
    private String normalizeContextPath(final String contextPath) {
      final String separator = ServletContextUriLocator.PREFIX;
      final StringBuffer sb = new StringBuffer(separator);
      if (contextPath != null) {
        String normalizedContextPath = contextPath;
        normalizedContextPath = StringUtils.removeStart(normalizedContextPath, separator);
        normalizedContextPath = StringUtils.removeEnd(normalizedContextPath, separator);
        sb.append(normalizedContextPath);
      }
      return sb.toString();
    }
  
    private AggregatedFolderPathResolver getAggregatedPathResolver() {
      return new AggregatedFolderPathResolver().setBuildDirectory(buildDirectory).setBuildFinalName(buildFinalName).setContextFoldersAsCSV(
          getContextFoldersAsCSV()).setCssDestinationFolder(cssDestinationFolder).setDestinationFolder(destinationFolder).setLog(
          getLog());
    }
  
    /**
     * @param destinationFolder
     *          the destinationFolder to set
     * @VisibleForTesting
     */
    void setDestinationFolder(final File destinationFolder) {
      this.destinationFolder = destinationFolder;
    }
  
    /**
     * @param cssDestinationFolder
     *          the cssDestinationFolder to set
     * @VisibleForTesting
     */
    void setCssDestinationFolder(final File cssDestinationFolder) {
      this.cssDestinationFolder = cssDestinationFolder;
    }
  
    /**
     * @param jsDestinationFolder
     *          the jsDestinationFolder to set
     * @VisibleForTesting
     */
    void setJsDestinationFolder(final File jsDestinationFolder) {
      this.jsDestinationFolder = jsDestinationFolder;
    }
  
    /**
     * The folder where the project is built.
     *
     * @param buildDirectory
     *          the buildDirectory to set
     * @VisibleForTesting
     */
    void setBuildDirectory(final File buildDirectory) {
      this.buildDirectory = buildDirectory;
    }
  
    /**
     * @param buildFinalName
     *          the buildFinalName to set
     */
    public void setBuildFinalName(final File buildFinalName) {
      this.buildFinalName = buildFinalName;
    }
  
    /**
     * @param groupNameMappingFile
     *          the groupNameMappingFile to set
     * @VisibleForTesting
     */
    void setGroupNameMappingFile(final File groupNameMappingFile) {
      this.groupNameMappingFile = groupNameMappingFile;
    }
  
    /**
     * @VisibleForTesting
     */
    void setContextPath(final String contextPath) {
      this.contextPath = contextPath;
    }
  }
  