/** * 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; } }