/**
* Copyright 2014 55 Minutes (http://www.55minutes.com)
*
* 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 fiftyfive.wicket.resource;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
import org.apache.wicket.util.IProvider;
/**
* Provides a simple builder API for constructing and mounting a virtual resource that merges
* together several actual resources. This is a common web optimization technique for reducing
* browser requests: instead of the browser making several small requests to download various
* CSS or JavaScript files, the browser can instead make one large request. Since each request
* brings its own latency and HTTP overhead, merging resources together can therefore lead to a
* noticable performance improvement.
* <p>
* The actual merging is done by {@link MergedResourceMapper} and its helper class,
* {@link MergedResourceRequestHandler}. This {@code MergedJavaScriptBuilder} class simply provides
* a builder API that makes constructing and mounting the mapper more self-explanatory. Advanced
* users may wish to use the mapper directly.
* <p>
* Note that this class is abstract. Refer to the concrete subclasses
* {@link fiftyfive.wicket.js.MergedJavaScriptBuilder MergedJavaScriptBuilder} and
* {@link fiftyfive.wicket.css.MergedCssBuilder MergedCssBuilder} for example usage.
* <p>
* <em>This class was rewritten in fiftyfive-wicket 3.0 to remove all dependencies on
* third-party libraries. The wicketstuff-merged-resources project is no longer used.
* Unlike previous versions that merged resources only in deployment mode, this implementation
* always merges resources, both in deployment and development modes.</em>
*/
public abstract class MergedResourceBuilder
{
private String path;
private boolean frozen = false;
private List<ResourceReference> references;
public MergedResourceBuilder()
{
this.references = new ArrayList<ResourceReference>();
}
/**
* Sets the path at which the merged resources will be mounted.
* For example, "styles/all.css".
*
* @return {@code this} for chaining
*/
public MergedResourceBuilder setPath(String path)
{
this.path = path;
return this;
}
/**
* @deprecated Please use {@link #install install()} instead.
*/
public Behavior build(WebApplication app)
{
install(app);
return buildHeaderContributor();
}
/**
* Constructs a special merged resource using the path and resources options specified in this
* builder, and mounts the result in the application by calling
* {@link WebApplication#mount(IRequestMapper) WebApplication.mount()}.
* <p>
* This method may only be called after all of the options have been set.
*
* @return {@code this} for chaining
*
* @throws IllegalStateException if a path or resources have not been
* specified prior to calling this method.
*
* @since 3.0
*/
public MergedResourceBuilder install(WebApplication app)
{
app.mount(buildRequestMapper(app));
return this;
}
/**
* Constructs and returns a special merged resource request mapper using the path and resources
* options specified in this builder.
* <p>
* This method may only be called after all of the options have been set.
* <p>
* Use this method if your application has a complex configuration that requires you to deal
* with request mappers directly (e.g. you need to wrap or combine them in clever ways).
* Most applications will be better served by {@link #install install()}, which
* handles creating the mapper and mounting it in one easy step.
*
* @throws IllegalStateException if a path or resources have not been
* specified prior to calling this method.
*
* @since 3.0
*/
public IRequestMapper buildRequestMapper(final WebApplication app)
{
if(!this.frozen) assertRequiredOptionsAndFreeze();
return new MergedResourceMapper(
this.path,
this.references,
new PageParametersEncoder(),
new IProvider<IResourceCachingStrategy>()
{
public IResourceCachingStrategy get()
{
return app.getResourceSettings().getCachingStrategy();
}
});
}
/**
* Constructs and returns a {@link Behavior} that will contribute all resources of this
* builder to the {@code <head>}. This could be useful on your base page, for example, to
* ensure that all pages of your app have a common set of resources.
*
* @since 3.0
*/
public Behavior buildHeaderContributor()
{
if(!this.frozen) assertRequiredOptionsAndFreeze();
return new Behavior() {
@Override
public void renderHead(Component comp, IHeaderResponse response)
{
for(int i=0; i<MergedResourceBuilder.this.references.size(); i++)
{
ResourceReference ref = MergedResourceBuilder.this.references.get(i);
newContributor(ref).renderHead(comp, response);
}
}
};
}
/**
* Add a resource to the list of merged resources.
*
* @since 2.0
*/
protected void add(ResourceReference ref)
{
if(this.frozen)
{
throw new IllegalStateException(
"Resources cannot be added once build() or install() methods have been called.");
}
this.references.add(ref);
}
/**
* Constructs a header contributor for the given resource.
* Subclasses should implement the appropriate CSS or JS contributor.
*
* @since 2.0
*/
protected abstract Behavior newContributor(ResourceReference ref);
/**
* Called when one of the build or install methods is invoked to verify that all required
* properties have been provided. After this method is called the builder will be considered
* "frozen"; that is, no more resources may be added.
*
* @throws IllegalStateException if the path has not been set or if no resources have been
* added to the builder
*/
protected void assertRequiredOptionsAndFreeze()
{
if(null == this.path)
{
throw new IllegalStateException("path must be set");
}
if(this.references.size() == 0)
{
throw new IllegalStateException(
"at least one resource must be added"
);
}
this.frozen = true;
}
}