/*
* 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.resource.writer.impl;
import java.io.*;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import javax.faces.application.Resource;
import com.google.common.io.ByteSource;
import com.google.common.io.FileWriteMode;
import com.google.common.io.Files;
import org.richfaces.log.Logger;
import org.richfaces.resource.ResourceKey;
import org.richfaces.resource.ResourceSkinUtils;
import org.richfaces.resource.optimizer.ResourceWriter;
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.strings.Constants;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* @author Nick Belaevski
*/
public class ResourceWriterImpl implements ResourceWriter {
private static final class ResourceInputStreamSupplier extends ByteSource {
private Resource resource;
public ResourceInputStreamSupplier(Resource resource) {
super();
this.resource = resource;
}
@Override
public InputStream openStream() throws IOException {
return resource.getInputStream();
}
}
/*
* packed output stream by extension
*/
private final Map<String, OutputStream> PACKED = new LinkedHashMap<String, OutputStream>();
private File resourceContentsDir;
private Map<String, String> processedResources = Maps.newConcurrentMap();
private Iterable<ResourceProcessor> resourceProcessors;
private Logger log;
private long currentTime;
private Set<ResourceKey> resourcesWithKnownOrder;
private Set<ResourceKey> packedResources = Sets.newHashSet();
public ResourceWriterImpl(File resourceContentsDir, Iterable<ResourceProcessor> resourceProcessors, Logger log,
Set<ResourceKey> resourcesWithKnownOrder) {
this.resourceContentsDir = resourceContentsDir;
this.resourceProcessors = Iterables.concat(resourceProcessors,
Collections.singleton(ThroughputResourceProcessor.INSTANCE));
this.log = log;
this.resourcesWithKnownOrder = resourcesWithKnownOrder;
resourceContentsDir.mkdirs();
currentTime = System.currentTimeMillis();
}
private synchronized File createOutputFile(String path) throws IOException {
File outFile = new File(resourceContentsDir, path);
outFile.getParentFile().mkdirs();
if (outFile.exists()) {
if (outFile.lastModified() > currentTime) {
log.debug(MessageFormat.format("File {0} already exists and will be overwritten", outFile.getPath()));
}
outFile.delete();
}
if (!outFile.createNewFile()) {
log.warn(MessageFormat.format("Could not create {0} file", outFile.getPath()));
}
return outFile;
}
public void writeResource(String skinName, Resource resource) throws IOException {
final String requestPath = resource.getRequestPath();
final String requestPathWithSkin = skinName == null ? requestPath : ResourceSkinUtils.evaluateSkinInPath(requestPath,
skinName);
ResourceProcessor matchingProcessor = getMatchingResourceProcessor(requestPath);
File outFile = createOutputFile(requestPathWithSkin);
log.debug("Opening output stream for " + outFile);
matchingProcessor.process(requestPathWithSkin, new ResourceInputStreamSupplier(resource).openStream(),
Files.asByteSink(outFile).openStream(), true);
processedResources.put(ResourceUtil.getResourceQualifier(resource), requestPath);
}
public void writePackedResource(String packName, String skinName, Resource resource) throws IOException {
final String requestPath = resource.getRequestPath();
String extension = getExtension(requestPath);
String packFileName = packName + "." + extension;
ResourceKey resourceKey = new ResourceKey(resource.getResourceName(), resource.getLibraryName());
if (!"js".equals(extension) && !"css".equals(extension)) {
writeResource(skinName, resource);
return;
}
if (!resourcesWithKnownOrder.contains(resourceKey)) {
writeResource(skinName, resource);
return;
}
String requestPathWithSkinVariable = "packed/" + packFileName;
if (skinName != null && skinName.length() > 0) {
requestPathWithSkinVariable = ResourceSkinUtils.prefixPathWithSkinPlaceholder(requestPathWithSkinVariable);
}
String requestPathWithSkin = Constants.SLASH_JOINER.join(skinName, "packed", packFileName);
ResourceProcessor matchingProcessor = getMatchingResourceProcessor(requestPathWithSkin);
OutputStream outputStream;
synchronized (PACKED) {
String packagingCacheKey = extension + ":" + skinName;
if (!PACKED.containsKey(packagingCacheKey)) {
File outFile = createOutputFile(requestPathWithSkin);
log.debug("Opening shared output stream for " + outFile);
outputStream = Files.asByteSink(outFile, FileWriteMode.APPEND).openStream();
PACKED.put(packagingCacheKey, outputStream);
}
outputStream = PACKED.get(packagingCacheKey);
}
synchronized (outputStream) {
matchingProcessor.process(requestPathWithSkin, resource.getInputStream(), outputStream,
false);
}
processedResources.put(ResourceUtil.getResourceQualifier(resource), requestPathWithSkinVariable);
packedResources.add(resourceKey);
// when packaging JSF's JavaScript, make sure both compressed and uncompressed are written to static mappings
if (ResourceUtil.isSameResource(resource, ResourceConstants.JSF_UNCOMPRESSED)
|| ResourceUtil.isSameResource(resource, ResourceConstants.JSF_COMPRESSED)) {
processedResources.put(ResourceUtil.getResourceQualifier(ResourceConstants.JSF_COMPRESSED),
requestPathWithSkinVariable);
processedResources.put(ResourceUtil.getResourceQualifier(ResourceConstants.JSF_UNCOMPRESSED),
requestPathWithSkinVariable);
}
}
private ResourceProcessor getMatchingResourceProcessor(final String requestPath) {
return Iterables.get(Iterables.filter(resourceProcessors, new Predicate<ResourceProcessor>() {
@Override
public boolean apply(ResourceProcessor input) {
return input.isSupportedFile(requestPath);
}
}), 0);
}
private String getExtension(String requestPath) {
int extensionIndex = Math.max(requestPath.lastIndexOf('.'), requestPath.lastIndexOf('/'));
return requestPath.substring(extensionIndex + 1);
}
@Override
public void writeProcessedResourceMappings(File staticResourceMappingFile, String staticResourcePrefix) throws IOException {
// TODO separate mappings file location
FileOutputStream fos = null;
try {
if (!staticResourceMappingFile.exists()) {
staticResourceMappingFile.getParentFile().mkdirs();
staticResourceMappingFile.createNewFile();
}
fos = new FileOutputStream(staticResourceMappingFile, true);
Properties properties = new Properties();
for (Entry<String, String> entry : processedResources.entrySet()) {
properties.put(entry.getKey(), staticResourcePrefix + entry.getValue());
}
// properties.putAll(processedResources);
properties.store(fos, null);
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
// TODO: handle exception
}
}
}
public void close() {
for (OutputStream out : PACKED.values()) {
try {
out.close();
} catch (IOException e) {
// Swallow
}
}
}
}