/*
* Copyright 2012 Joseph Spencer.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.spencernetdevelopment;
import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.spencernetdevelopment.Logger.*;
/**
*
* @author Joseph Spencer
*/
public class AssetManager {
private static final Pattern CSS_URL = Pattern.compile("url\\(\\s*(['\"])?/((?:(?!\\1\\)).)+)\\1?\\)");
private final String assetPrefixInBrowser;
private final int maxDataURISizeInBytes;
private final FilePath assetPath;
private final FilePath buildPath;
private final AssetResolver assetResolver;
private final FileUtils fileUtils;
private final StaticPagesConfiguration config;
private final Object TRANSFER_ASSET_LOCK = new Object();
public AssetManager(
FilePath assets,
FilePath build,
FileUtils fileUtils,
StaticPagesConfiguration config,
AssetResolver assetResolver
)
throws IOException
{
assetPath=assets;
buildPath=build;
this.assetPrefixInBrowser=config.getAssetPrefixInBrowser();
this.maxDataURISizeInBytes=config.getMaxDataURISizeInBytes();
this.assetResolver=assetResolver;
this.fileUtils=fileUtils;
this.config=config;
}
public String getAsset(File file) throws IOException {
return fileUtils.getString(file);
}
public String getAsset(String path) throws IOException {
if(isDebug)debug("getAsset called with path: "+path);
File file = assetPath.resolve(path).toFile();
return getAsset(file);
}
public String getCSS(File file, boolean compress) throws IOException, URISyntaxException {
return handleCSS(getAsset(file), compress);
}
public String getCSS(String path, String compress) throws IOException, URISyntaxException {
return getCSS(path, isCompressionFromAttributeValue(compress));
}
public String getCSS(String path, boolean compress) throws IOException, URISyntaxException {
if(isDebug)debug("getCSS called with path: "+path);
return handleCSS(getAsset(
assetResolver.getCleanCSSPath(path)
), compress);
}
private String handleCSS(String contents, boolean compress) throws IOException, URISyntaxException {
String contentsToReturn;
if(compress){
StringReader reader = new StringReader(contents);
StringWriter writer = new StringWriter(contents.length());
CssCompressor cssCompressor = new com.yahoo.platform.yui.compressor.CssCompressor(reader);
cssCompressor.compress(writer, -1);
contentsToReturn = writer.toString();
} else {
contentsToReturn = contents;
}
//Use this to ensure we don't keep replacing the same URL over and over
//again in the css file when we're not going to embed the data uri.
Set<String> seive = new HashSet<>();
//This transferes all images over that are defined in css files.
Matcher urls = CSS_URL.matcher(contents);
while(urls.find()){
String url = new URI(urls.group(2)).getPath();
String dataType=url.toLowerCase().replaceFirst(".*\\.([^\\.]+)$", "$1");
String mimeType=null;
if(compress){
switch(dataType){
case "gif":
case "jpeg":
case "jpg":
case "png":
mimeType="image/"+dataType;
break;
}
if(mimeType != null){
byte[] bytes = fileUtils.getBytes(
assetPath.resolve(url).toFile()
);
String encoded = Base64.encodeToString(bytes, false);
byte[] encodedBytes = encoded.getBytes();
if(maxDataURISizeInBytes > 0 &&
encodedBytes.length <= maxDataURISizeInBytes
){
contentsToReturn = contentsToReturn.replace(
urls.group(0),
"url(data:"+mimeType+";base64,"+encoded+")"
);
continue;
} else {
Logger.info(
"The following resource exceeded the max size in bytes "+
"specified and can not be embedded within CSS: "+url);
}
}
}
if(!seive.contains(url)){
seive.add(url);
contentsToReturn = contentsToReturn.replace(
"/"+url,
assetPrefixInBrowser+"/"+assetResolver.getAssetPath(url)
);
transferAsset(url, assetResolver.getAssetPath(url));
}
}
return contentsToReturn;
}
public String getJS(File file, boolean compress) throws IOException {
return handleJS(getAsset(file), compress);
}
public String getJS(String path, String compress) throws IOException, URISyntaxException {
return getJS(path, isCompressionFromAttributeValue(compress));
}
public String getJS(String path, boolean compress) throws IOException, URISyntaxException {
if(isDebug)debug("getJS called with path: "+path);
return handleJS(getAsset(
assetResolver.getCleanJSPath(path)
), compress);
}
private String handleJS(String contents, boolean compress) throws IOException {
String minified=null;
if(compress){
StringReader reader = new StringReader(contents);
StringWriter writer = new StringWriter(contents.length());
JavaScriptCompressor javaScriptCompressor = new JavaScriptCompressor(reader, JSErrorReporter.INSTANCE);
javaScriptCompressor.compress(writer, -1, true, false, false, false);
minified=writer.toString();
}
return (minified == null ? contents : minified).replace(
"ASSET_PREFIX_IN_BROWSER",
assetPrefixInBrowser
);
}
public void transferCSS(String src, String compress)
throws IOException,
URISyntaxException
{
transferCSS(src,isCompressionFromAttributeValue(compress));
}
public void transferCSS(String src, boolean compress)
throws IOException,
URISyntaxException
{
transferCSS(
assetResolver.getCleanCSSPath(src),
assetResolver.getCSSPath(src),
compress
);
}
public void transferCSS(
String srcPath,
String targetPath,
boolean compress
) throws
IOException, URISyntaxException
{
AtomicReference<File> source = new AtomicReference<>();
AtomicReference<File> target = new AtomicReference<>();
if(prepareAssetTransfer(
srcPath,
source,
targetPath,
target
)){
if(isDebug)debug(
"transferCSS called with path: "+srcPath+"\n"+
"and compress: "+compress
);
String css = getCSS(source.get(), compress);
fileUtils.putString(target.get(), css);
}
}
public void transferImage(String path)
throws IOException,
URISyntaxException
{
transferAsset(path, assetResolver.getAssetPath(path));
}
public void transferJS(String src, String compress)
throws IOException,
URISyntaxException
{
transferJS(src, isCompressionFromAttributeValue(compress));
}
public void transferJS(String src, boolean compress)
throws IOException,
URISyntaxException
{
transferJS(
assetResolver.getCleanJSPath(src),
assetResolver.getJSPath(src),
compress);
}
public void transferJS(
String srcPath,
String targetPath,
boolean compress
) throws
IOException
{
AtomicReference<File> source = new AtomicReference<>();
AtomicReference<File> target = new AtomicReference<>();
if(prepareAssetTransfer(
srcPath,
source,
targetPath,
target
)){
if(isDebug)debug(
"transferJS called with path: "+srcPath+"\n"+
"and compress: "+compress
);
fileUtils.putString(target.get(), getJS(source.get(), compress));
}
}
/**
* Transfers assets from the src dir to the build dir.
*
* @param path
* @throws IOException
*/
public void transferAsset(String path) throws IOException {
transferAsset(path, path);
}
/**
* Transfers assets as char arrays.
*
* @param path
* @throws IOException
*/
public void transferAsset(
String srcPath,
String targetPath
) throws
IOException
{
srcPath = assetPath.resolve(srcPath).toString();
targetPath = buildPath.resolve(targetPath).toString();
if(fileUtils.isDirectory(srcPath)){
fileUtils.copyDirContentsToDir(srcPath, targetPath);
} else {
AtomicReference<File> source = new AtomicReference<>();
AtomicReference<File> target = new AtomicReference<>();
if(prepareAssetTransfer(srcPath, source, targetPath, target)){
if(isDebug)debug("transferAsset called with path: "+srcPath);
fileUtils.copyFile(source.get(), target.get());
}
}
}
/**
* Prepares assets for transferring if the source is newer than the target.
*
* @param path
* @return true the asset can be transferred.
*/
private boolean prepareAssetTransfer(
String srcPath,
AtomicReference<File> source,
String targetPath,
AtomicReference<File> target
) throws
IOException
{
File from = assetPath.resolve(srcPath).toFile();
File to = buildPath.resolve(targetPath).toFile();
String fromPath = from.getAbsolutePath();
String toPath = to.getAbsolutePath();
String preamble = "Couldn't transfer the following asset because it";
if(!from.exists()){
throw new IOException(preamble+" doesn't exist: "+fromPath);
}
if(!from.isFile()){
throw new IOException(preamble+" isn't a file: "+fromPath);
}
if(to.isDirectory()){
throw new IOException(preamble+"'s target under build is a directory: "+toPath);
}
synchronized(TRANSFER_ASSET_LOCK){
if(from.lastModified() > to.lastModified() || !to.exists()){
source.set(from);
target.set(to);
return true;
}
return false;
}
}
public boolean isCompressionFromAttributeValue(String bool){
return isBoolean(bool, config.isEnableCompression());
}
public boolean isBoolean(String bool, boolean fallback){
if("true".equals(bool)){
return true;
} else if("false".equals(bool)){
return false;
} else {
return fallback;
}
}
}