/* * JBoss, Home of Professional Open Source * Copyright 2013, Red Hat, Inc. and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.richfaces.resource.optimizer; import static com.google.common.base.Predicates.notNull; import static com.google.common.collect.Collections2.filter; import static com.google.common.collect.Collections2.transform; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import javax.faces.application.Resource; import javax.faces.application.ResourceDependency; import org.richfaces.application.Module; import org.richfaces.application.ServicesFactoryImpl; import org.richfaces.log.Logger; import org.richfaces.log.RichfacesLogger; import org.richfaces.resource.ResourceFactory; import org.richfaces.resource.ResourceFactoryImpl; import org.richfaces.resource.ResourceKey; import org.richfaces.resource.optimizer.concurrent.CountingExecutorCompletionService; import org.richfaces.resource.optimizer.faces.FacesImpl; import org.richfaces.resource.optimizer.faces.ServiceFactoryModule; import org.richfaces.resource.optimizer.naming.FileNameMapperImpl; import org.richfaces.resource.optimizer.resource.handler.impl.DynamicResourceHandler; import org.richfaces.resource.optimizer.resource.handler.impl.StaticResourceHandler; import org.richfaces.resource.optimizer.resource.scan.ResourcesScanner; import org.richfaces.resource.optimizer.resource.scan.impl.DynamicResourcesScanner; import org.richfaces.resource.optimizer.resource.scan.impl.ResourceOrderingScanner; import org.richfaces.resource.optimizer.resource.scan.impl.StaticResourcesScanner; import org.richfaces.resource.optimizer.resource.util.ResourceConstants; import org.richfaces.resource.optimizer.resource.util.ResourceUtil; import org.richfaces.resource.optimizer.resource.writer.ResourceProcessor; import org.richfaces.resource.optimizer.resource.writer.impl.CSSCompressingProcessor; import org.richfaces.resource.optimizer.resource.writer.impl.CSSPackagingProcessor; import org.richfaces.resource.optimizer.resource.writer.impl.JavaScriptCompressingProcessor; import org.richfaces.resource.optimizer.resource.writer.impl.JavaScriptPackagingProcessor; import org.richfaces.resource.optimizer.resource.writer.impl.ResourceWriterImpl; import org.richfaces.resource.optimizer.strings.Constants; import org.richfaces.resource.optimizer.task.ResourceTaskFactoryImpl; import org.richfaces.resource.optimizer.util.MorePredicates; import org.richfaces.resource.optimizer.vfs.VFS; import org.richfaces.resource.optimizer.vfs.VFSRoot; import org.richfaces.resource.optimizer.vfs.VirtualFile; import org.richfaces.application.ServiceTracker; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; /** * Configurable command-line interface of CDK generator. * * This class is similar functionality as {@link org.richfaces.builder.mojo.GenerateMojo} from richfaces-cdk-maven-plugin. * * @author Lukas Fryc */ @Parameters(resourceBundle = "resource-optimizer-cmdln-usage") public class ResourceGenerator { private static final Logger log = RichfacesLogger.RESOURCE.getLogger(); private static final URL[] EMPTY_URL_ARRAY = new URL[0]; private static final Function<String, Predicate<CharSequence>> REGEX_CONTAINS_BUILDER_FUNCTION = new Function<String, Predicate<CharSequence>>() { public Predicate<CharSequence> apply(String from) { Predicate<CharSequence> containsPredicate = Predicates.containsPattern(from); return Predicates.and(Predicates.notNull(), containsPredicate); } }; private static final Function<Resource, String> CONTENT_TYPE_FUNCTION = new Function<Resource, String>() { public String apply(Resource from) { return from.getContentType(); } }; private static final Function<Resource, String> RESOURCE_QUALIFIER_FUNCTION = new Function<Resource, String>() { public String apply(Resource from) { return ResourceUtil.getResourceQualifier(from); } }; private final Function<String, URL> filePathToURL = new Function<String, URL>() { public URL apply(String from) { try { File file = new File(from); if (file.exists()) { return file.toURI().toURL(); } } catch (MalformedURLException e) { log.error("Bad URL in classpath", e); } return null; } }; /** * Request print of usage */ @Parameter(names = { "--help" }, descriptionKey = "help") private boolean help = false; /** * Output directory for processed resources */ @Parameter(names = { "-o", "--output" }, descriptionKey = "resourcesOutputDir", required = true) private String resourcesOutputDir; /** * Configures what prefix should be placed to each file before the library and name of the resource */ @Parameter(names = { "-p", "--prefix" }, descriptionKey = "staticResourcePrefix", required = true) private String staticResourcePrefix; /** * Output file for resource mapping configuration */ @Parameter(names = { "-m", "--mapping" }, descriptionKey = "staticResourceMappingFile", required = true) private String staticResourceMappingFile; /** * The list of RichFaces skins to be processed */ @Parameter(names = { "-s", "--skins" }, descriptionKey = "skins", required = true) private String skins; /** * Output directory for processed resources */ @Parameter(names = { "-c", "--classpathDir" }, descriptionKey = "classpathDir", required = true) private File classpathDir; /** * The list of mime-types to be included in processing */ @Parameter(names = { "--includeContentType" }, descriptionKey = "includedContentTypes") private List<String> includedContentTypes = Arrays.asList("application/javascript", "text/css", "image/.+"); /** * The list of mime-types to be excluded in processing */ @Parameter(names = { "--excludeContentType" }, descriptionKey = "excludedContentTypes") private List<String> excludedContentTypes = Arrays.asList(); /** * List of included files. */ @Parameter(names = { "--includeFile" }, descriptionKey = "includedFiles") private List<String> includedFiles = Arrays.asList(); /** * List of excluded files */ @Parameter(names = { "--excludeFile" }, descriptionKey = "excludedFiles") private List<String> excludedFiles = Arrays.asList("^javax.faces", "^\\Qorg.richfaces.renderkit.html.images.\\E.*", "^\\Qorg.richfaces.renderkit.html.iconimages.\\E.*"); /** * Turns on compression with YUI Compressor (JavaScript/CSS compression) */ @Parameter(names = { "--compress" }, descriptionKey = "compress") private boolean compress = false; /** * Turns on packing of JavaScript/CSS resources */ @Parameter(names = { "--pack" }, descriptionKey = "pack") private String pack; /** * Mapping of file names to output file names */ private FileNameMapping[] fileNameMappings = new FileNameMapping[] { new FileNameMapping("^.*showcase.*/([^/]+\\.css)$", "org.richfaces.showcase.css/$1"), new FileNameMapping("^.+/([^/]+\\.(png|gif|jpg))$", "org.richfaces.images/$1"), new FileNameMapping("^.+/([^/]+\\.css)$", "org.richfaces.css/$1") }; /** * The expression determines the root of the webapp resources */ private String webRoot = new File("./src/main/webapp").getAbsolutePath(); /** * The encoding used for resource processing */ private String encoding = "UTF-8"; private Collection<ResourceKey> foundResources = Sets.newHashSet(); private Ordering<ResourceKey> resourceOrdering; private Set<ResourceKey> resourcesWithKnownOrder; private ExecutorService createExecutorService() { int poolSize = (pack != null) ? 1 : Runtime.getRuntime().availableProcessors(); return Executors.newFixedThreadPool(poolSize); } private Collection<ResourceProcessor> getDefaultResourceProcessors() { Charset charset = Charset.defaultCharset(); if (!Strings.isNullOrEmpty(encoding)) { charset = Charset.forName(encoding); } else { log.warn( "Encoding is not set explicitly, CDK resources plugin will use default platform encoding for processing char-based resources"); } if (compress) { return Arrays.<ResourceProcessor>asList(new JavaScriptCompressingProcessor(charset, log), new CSSCompressingProcessor( charset)); } else { return Arrays.<ResourceProcessor>asList(new JavaScriptPackagingProcessor(charset), new CSSPackagingProcessor(charset)); } } private Predicate<Resource> createResourcesFilter() { Predicate<CharSequence> qualifierPredicate = MorePredicates.compose(includedFiles, excludedFiles, REGEX_CONTAINS_BUILDER_FUNCTION); Predicate<Resource> qualifierResourcePredicate = Predicates.compose(qualifierPredicate, RESOURCE_QUALIFIER_FUNCTION); Predicate<CharSequence> contentTypePredicate = MorePredicates.compose(includedContentTypes, excludedContentTypes, REGEX_CONTAINS_BUILDER_FUNCTION); Predicate<Resource> contentTypeResourcePredicate = Predicates.compose(contentTypePredicate, CONTENT_TYPE_FUNCTION); return Predicates.and(qualifierResourcePredicate, contentTypeResourcePredicate); } private URL resolveWebRoot() throws MalformedURLException { File result = new File(webRoot); if (!result.exists()) { result = new File(".", webRoot); } if (!result.exists()) { return null; } return result.toURI().toURL(); } private void scanDynamicResources(Collection<VFSRoot> cpFiles, ResourceFactory resourceFactory) throws Exception { ResourcesScanner scanner = new DynamicResourcesScanner(cpFiles, resourceFactory); scanner.scan(); foundResources.addAll(scanner.getResources()); } private void scanStaticResources(Collection<VirtualFile> resourceRoots) throws Exception { ResourcesScanner scanner = new StaticResourcesScanner(resourceRoots); scanner.scan(); foundResources.addAll(scanner.getResources()); } private void scanResourceOrdering(Collection<VFSRoot> cpFiles) throws Exception { ResourceOrderingScanner scanner = new ResourceOrderingScanner(cpFiles, log); scanner.scan(); resourceOrdering = scanner.getCompleteOrdering(); resourcesWithKnownOrder = Sets.newLinkedHashSet(scanner.getResources()); } private Collection<VFSRoot> fromUrls(Iterable<URL> urls) throws URISyntaxException, IOException { Collection<VFSRoot> result = Lists.newArrayList(); for (URL url : urls) { if (url == null) { continue; } VFSRoot vfsRoot = VFS.getRoot(url); vfsRoot.initialize(); result.add(vfsRoot); } return result; } private Collection<VFSRoot> getClasspathVfs(URL[] urls) throws URISyntaxException, IOException { return fromUrls(Arrays.asList(urls)); } private Collection<VFSRoot> getWebrootVfs() throws URISyntaxException, IOException { return fromUrls(Collections.singletonList(resolveWebRoot())); } protected URL[] getProjectClassPath() { List<String> classpath = new ArrayList<String>(); classpath.add(classpathDir.getAbsolutePath()); URL[] urlClasspath = filter(transform(classpath, filePathToURL), notNull()).toArray(EMPTY_URL_ARRAY); return urlClasspath; } protected ClassLoader createProjectClassLoader(URL[] cp) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); classLoader = new URLClassLoader(cp, classLoader); return classLoader; } /** * Initializes {@link ServiceTracker} to be able use it inside RichFaces framework code * in order to handle dynamic resources. * * Fake {@link ServiceFactoryModule} is used for this purpose. * * @throws IllegalStateException when initialization fails */ private void initializeServiceTracker() { ServicesFactoryImpl servicesFactory = new ServicesFactoryImpl(); ServiceTracker.setFactory(servicesFactory); ArrayList<Module> modules = new ArrayList<Module>(); modules.add(new ServiceFactoryModule()); // try { // modules.addAll(ServiceLoader.loadServices(Module.class)); // } catch (ServiceException e) { // throw new IllegalStateException(e); // } servicesFactory.init(modules); } /** * Will determine ordering of resources from {@link ResourceDependency} annotations on renderers. * * Sorts foundResources using the determined ordering. */ private void reorderFoundResources(Collection<VFSRoot> cpResources, DynamicResourceHandler dynamicResourceHandler, ResourceFactory resourceFactory) throws Exception { Faces faces = new FacesImpl(null, new FileNameMapperImpl(fileNameMappings), dynamicResourceHandler); faces.start(); initializeServiceTracker(); // if there are some resource libraries (.reslib), we need to expand them foundResources = new ResourceLibraryExpander().expandResourceLibraries(foundResources); faces.startRequest(); scanResourceOrdering(cpResources); faces.stopRequest(); faces.stop(); foundResources = resourceOrdering.sortedCopy(foundResources); // do not package two versions of JFS JavaScript (remove it from resources for packaging) foundResources.remove(ResourceConstants.JSF_UNCOMPRESSED); // we need to load java.faces:jsf-uncompressed.js, but we will package resourcesWithKnownOrder.add(ResourceConstants.JSF_UNCOMPRESSED); log.debug("resourcesWithKnownOrder: " + resourcesWithKnownOrder); } public void execute() { ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); Faces faces = null; ExecutorService executorService = null; Collection<VFSRoot> webResources = null; Collection<VFSRoot> cpResources = null; try { URL[] projectCP = getProjectClassPath(); ClassLoader projectCL = createProjectClassLoader(projectCP); Thread.currentThread().setContextClassLoader(projectCL); webResources = getWebrootVfs(); cpResources = getClasspathVfs(projectCP); Collection<VirtualFile> resourceRoots = ResourceUtil.getResourceRoots(cpResources, webResources); log.debug("resourceRoots: " + resourceRoots); scanStaticResources(resourceRoots); StaticResourceHandler staticResourceHandler = new StaticResourceHandler(resourceRoots); ResourceFactory resourceFactory = new ResourceFactoryImpl(staticResourceHandler); scanDynamicResources(cpResources, resourceFactory); DynamicResourceHandler dynamicResourceHandler = new DynamicResourceHandler(staticResourceHandler, resourceFactory); log.debug("foundResources: " + foundResources); if (pack != null) { reorderFoundResources(cpResources, dynamicResourceHandler, resourceFactory); } faces = new FacesImpl(null, new FileNameMapperImpl(fileNameMappings), dynamicResourceHandler); faces.start(); ResourceWriterImpl resourceWriter = new ResourceWriterImpl(new File(resourcesOutputDir), getDefaultResourceProcessors(), log, resourcesWithKnownOrder); ResourceTaskFactoryImpl taskFactory = new ResourceTaskFactoryImpl(faces, pack); taskFactory.setResourceWriter(resourceWriter); executorService = createExecutorService(); CompletionService<Object> completionService = new CountingExecutorCompletionService<Object>(executorService); taskFactory.setCompletionService(completionService); taskFactory.setSkins(Iterables.toArray(Constants.COMMA_SPLITTER.split(skins), String.class)); taskFactory.setLog(log); taskFactory.setFilter(createResourcesFilter()); taskFactory.submit(foundResources); log.debug(completionService.toString()); Future<Object> future = null; while (true) { future = completionService.take(); if (future != null) { try { future.get(); } catch (ExecutionException e) { log.error(e); } } else { break; } } log.debug(completionService.toString()); resourceWriter.writeProcessedResourceMappings(new File(staticResourceMappingFile), staticResourcePrefix); resourceWriter.close(); } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } finally { if (cpResources != null) { for (VFSRoot vfsRoot : cpResources) { try { vfsRoot.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } if (webResources != null) { for (VFSRoot vfsRoot : webResources) { try { vfsRoot.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } // TODO review finally block if (executorService != null) { executorService.shutdown(); } if (faces != null) { try { faces.stop(); } catch (Exception e) { log.warn("Failed to tear Faces down", e); } } Thread.currentThread().setContextClassLoader(contextCL); } } public boolean isHelp() { return help; } }