// 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 org.apache.tapestry5.internal.services.assets;
import org.apache.tapestry5.Asset;
import org.apache.tapestry5.ContentType;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.ioc.Resource;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.assets.*;
import org.apache.tapestry5.services.javascript.JavaScriptStack;
import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
import org.apache.tapestry5.services.javascript.JavaScriptAggregationStrategy;
import org.apache.tapestry5.services.javascript.ModuleManager;
import java.io.*;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
public class JavaScriptStackAssemblerImpl implements JavaScriptStackAssembler
{
private static final ContentType JAVASCRIPT_CONTENT_TYPE = new ContentType("text/javascript;charset=utf-8");
private final ThreadLocale threadLocale;
private final ResourceChangeTracker resourceChangeTracker;
private final StreamableResourceSource streamableResourceSource;
private final JavaScriptStackSource stackSource;
private final AssetChecksumGenerator checksumGenerator;
private final ModuleManager moduleManager;
private final ResourceMinimizer resourceMinimizer;
private final boolean minificationEnabled;
private final Map<String, StreamableResource> cache = CollectionFactory.newCaseInsensitiveMap();
private class Parameters
{
final Locale locale;
final String stackName;
final boolean compress;
final JavaScriptAggregationStrategy javascriptAggregationStrategy;
private Parameters(Locale locale, String stackName, boolean compress, JavaScriptAggregationStrategy javascriptAggregationStrategy)
{
this.locale = locale;
this.stackName = stackName;
this.compress = compress;
this.javascriptAggregationStrategy = javascriptAggregationStrategy;
}
Parameters disableCompress()
{
return new Parameters(locale, stackName, false, javascriptAggregationStrategy);
}
}
// TODO: Support for aggregated CSS as well as aggregated JavaScript
public JavaScriptStackAssemblerImpl(ThreadLocale threadLocale, ResourceChangeTracker resourceChangeTracker, StreamableResourceSource streamableResourceSource,
JavaScriptStackSource stackSource, AssetChecksumGenerator checksumGenerator, ModuleManager moduleManager,
ResourceMinimizer resourceMinimizer,
@Symbol(SymbolConstants.MINIFICATION_ENABLED)
boolean minificationEnabled)
{
this.threadLocale = threadLocale;
this.resourceChangeTracker = resourceChangeTracker;
this.streamableResourceSource = streamableResourceSource;
this.stackSource = stackSource;
this.checksumGenerator = checksumGenerator;
this.moduleManager = moduleManager;
this.resourceMinimizer = resourceMinimizer;
this.minificationEnabled = minificationEnabled;
resourceChangeTracker.clearOnInvalidation(cache);
}
public StreamableResource assembleJavaScriptResourceForStack(String stackName, boolean compress, JavaScriptAggregationStrategy javascriptAggregationStrategy) throws IOException
{
Locale locale = threadLocale.getLocale();
return assembleJavascriptResourceForStack(new Parameters(locale, stackName, compress, javascriptAggregationStrategy));
}
private StreamableResource assembleJavascriptResourceForStack(Parameters parameters) throws IOException
{
String key =
String.format("%s[%s] %s",
parameters.stackName,
parameters.compress ? "COMPRESS" : "UNCOMPRESSED",
parameters.locale.toString());
StreamableResource result = cache.get(key);
if (result == null)
{
result = assemble(parameters);
cache.put(key, result);
}
return result;
}
private StreamableResource assemble(Parameters parameters) throws IOException
{
if (parameters.compress)
{
StreamableResource uncompressed = assembleJavascriptResourceForStack(parameters.disableCompress());
return new CompressedStreamableResource(uncompressed, checksumGenerator);
}
JavaScriptStack stack = stackSource.getStack(parameters.stackName);
return assembleStreamableForStack(parameters.locale.toString(), parameters, stack.getJavaScriptLibraries(), stack.getModules());
}
interface StreamableReader
{
/**
* Reads the content of a StreamableResource as a UTF-8 string, and optionally transforms it in some way.
*/
String read(StreamableResource resource) throws IOException;
}
static String getContent(StreamableResource resource) throws IOException
{
final ByteArrayOutputStream bos = new ByteArrayOutputStream(resource.getSize());
resource.streamTo(bos);
return new String(bos.toByteArray(), "UTF-8");
}
final StreamableReader libraryReader = new StreamableReader()
{
public String read(StreamableResource resource) throws IOException
{
return getContent(resource);
}
};
private final static Pattern DEFINE = Pattern.compile("\\bdefine\\s*\\(");
private class ModuleReader implements StreamableReader
{
final String moduleName;
private ModuleReader(String moduleName)
{
this.moduleName = moduleName;
}
public String read(StreamableResource resource) throws IOException
{
String content = getContent(resource);
return DEFINE.matcher(content).replaceFirst("define(\"" + moduleName + "\",");
}
}
private class Assembly
{
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(2000);
final PrintWriter writer;
long lastModified = 0;
final StringBuilder description;
private String sep = "";
private Assembly(String description) throws UnsupportedEncodingException
{
writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"));
this.description = new StringBuilder(description);
}
void add(Resource resource, StreamableReader reader) throws IOException
{
writer.format("\n/* %s */;\n", resource.toString());
description.append(sep).append(resource.toString());
sep = ", ";
StreamableResource streamable = streamableResourceSource.getStreamableResource(resource,
StreamableResourceProcessing.FOR_AGGREGATION, resourceChangeTracker);
writer.print(reader.read(streamable));
lastModified = Math.max(lastModified, streamable.getLastModified());
}
StreamableResource finish()
{
writer.close();
return new StreamableResourceImpl(
description.toString(),
JAVASCRIPT_CONTENT_TYPE, CompressionStatus.COMPRESSABLE, lastModified,
new BytestreamCache(outputStream), checksumGenerator, null);
}
}
private StreamableResource assembleStreamableForStack(String localeName, Parameters parameters,
List<Asset> libraries, List<String> moduleNames) throws IOException
{
Assembly assembly = new Assembly(String.format("'%s' JavaScript stack, for locale %s, resources=", parameters.stackName, localeName));
for (Asset library : libraries)
{
Resource resource = library.getResource();
assembly.add(resource, libraryReader);
}
for (String moduleName : moduleNames)
{
Resource resource = moduleManager.findResourceForModule(moduleName);
if (resource == null)
{
throw new IllegalArgumentException(String.format("Could not identify a resource for module name '%s'.", moduleName));
}
assembly.add(resource, new ModuleReader(moduleName));
}
StreamableResource streamable = assembly.finish();
if (minificationEnabled && parameters.javascriptAggregationStrategy.enablesMinimize())
{
return resourceMinimizer.minimize(streamable);
}
return streamable;
}
}