content: /**
   * 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;
      }
  }
  