Package org.omnifaces.resourcehandler

Source Code of org.omnifaces.resourcehandler.CombinedResourceInfo$Builder

/*
* Copyright 2012 OmniFaces.
*
* 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.omnifaces.resourcehandler;

import static org.omnifaces.util.FacesLocal.getRequestDomainURL;
import static org.omnifaces.util.Utils.isEmpty;
import static org.omnifaces.util.Utils.serializeURLSafe;
import static org.omnifaces.util.Utils.unserializeURLSafe;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.faces.application.Resource;
import javax.faces.application.ResourceHandler;
import javax.faces.context.FacesContext;

import org.omnifaces.el.functions.Converters;
import org.omnifaces.util.Utils;

/**
* This class is a wrapper which collects all combined resources and stores it in the cache. A builder has been provided
* to create an instance of combined resource info and put it in the cache if absent.
* @author Bauke Scholtz
*/
final class CombinedResourceInfo {

  // Constants ------------------------------------------------------------------------------------------------------

  private static final Logger logger = Logger.getLogger(CombinedResourceHandler.class.getName());

  // ConcurrentHashMap was considered, but duplicate inserts technically don't harm and a HashMap is faster on read.
  private static final Map<String, CombinedResourceInfo> CACHE = new HashMap<>();

  private static final String LOG_RESOURCE_NOT_FOUND = "CombinedResourceHandler: The resource %s cannot be found"
      + " and therefore a 404 will be returned for the combined resource ID %s";

  // Properties -----------------------------------------------------------------------------------------------------

  private String id;
  private Set<ResourceIdentifier> resourceIdentifiers;
  private Set<Resource> resources;
  private int contentLength;
  private long lastModified;

  // Constructors ---------------------------------------------------------------------------------------------------

  /**
   * Creates an instance of combined resource info based on the given ID and ordered set of resource identifiers.
   * @param resourceIdentifiers Ordered set of resource identifiers, which are to be combined in a single resource.
   */
  private CombinedResourceInfo(String id, Set<ResourceIdentifier> resourceIdentifiers) {
    this.id = id;
    this.resourceIdentifiers = resourceIdentifiers;
  }

  /**
   * Use this builder to create an instance of combined resource info and put it in the cache if absent.
   * @author Bauke Scholtz
   */
  public static final class Builder {

    // Constants --------------------------------------------------------------------------------------------------

    private static final String ERROR_EMPTY_RESOURCES =
      "There are no resources been added. Use add() method to add them or use isEmpty() to check beforehand.";

    // Properties -------------------------------------------------------------------------------------------------

    private Set<ResourceIdentifier> resourceIdentifiers = new LinkedHashSet<>();

    // Actions ----------------------------------------------------------------------------------------------------

    /**
     * Add the resource represented by the given resource identifier resources of this combined resource info. The
     * insertion order is maintained and duplicates are filtered.
     * @param resourceIdentifier The resource identifier of the resource to be added.
     * @return This builder.
     */
    public Builder add(ResourceIdentifier resourceIdentifier) {
      resourceIdentifiers.add(resourceIdentifier);
      return this;
    }

    /**
     * Returns true if there are no resources been added. Use this method before {@link #create()} if it's unknown
     * if there are any resources been added.
     * @return True if there are no resources been added, otherwise false.
     */
    public boolean isEmpty() {
      return resourceIdentifiers.isEmpty();
    }

    /**
     * Creates the CombinedResourceInfo instance in cache if absent and return its ID.
     * @return The ID of the CombinedResourceInfo instance.
     * @throws IllegalStateException If there are no resources been added. So, to prevent it beforehand, use
     * the {@link #isEmpty()} method to check if there are any resources been added.
     */
    public String create() {
      if (resourceIdentifiers.isEmpty()) {
        throw new IllegalStateException(ERROR_EMPTY_RESOURCES);
      }

      return get(toUniqueId(resourceIdentifiers)).id;
    }

  }

  /**
   * Returns the combined resource info identified by the given ID from the cache. A new one will be created based on
   * the given ID if absent in cache.
   * @param id The ID of the combined resource info to be returned from the cache.
   * @return The combined resource info identified by the given ID from the cache.
   */
  public static CombinedResourceInfo get(String id) {
    CombinedResourceInfo info = CACHE.get(id);

    if (info == null) {
      Set<ResourceIdentifier> resourceIdentifiers = fromUniqueId(id);

      if (resourceIdentifiers != null) {
        info = new CombinedResourceInfo(id, Collections.unmodifiableSet(resourceIdentifiers));
        CACHE.put(id, info);
      }
    }

    return info;
  }

  // Actions --------------------------------------------------------------------------------------------------------

  /**
   * Lazily load the combined resources so that the set of resources, the total content length and the last modified
   * are been initialized. If one of the resources cannot be resolved, then this will log a WARNING and leave the
   * resources empty.
   */
  private synchronized void loadResources() {
    if (!isEmpty(resources)) {
      return;
    }

    FacesContext context = FacesContext.getCurrentInstance();
    ResourceHandler handler = context.getApplication().getResourceHandler();
    resources = new LinkedHashSet<>();
    contentLength = 0;
    lastModified = 0;

    for (ResourceIdentifier resourceIdentifier : resourceIdentifiers) {
      Resource resource = handler.createResource(resourceIdentifier.getName(), resourceIdentifier.getLibrary());

      if (resource == null) {
        logger.log(Level.WARNING, String.format(LOG_RESOURCE_NOT_FOUND, resourceIdentifier, id));
        resources.clear();
        return;
      }

      resources.add(resource);

      try {
        URLConnection connection;

        try {
          connection = resource.getURL().openConnection();
        }
        catch (Exception richFacesDoesNotSupportThis) {
          connection = new URL(getRequestDomainURL(context) + resource.getRequestPath()).openConnection();
        }

        contentLength += connection.getContentLength();
        long lastModified = connection.getLastModified();

        if (lastModified > this.lastModified) {
          this.lastModified = lastModified;
        }
      }
      catch (IOException e) {
        // Can't and shouldn't handle it at this point.
        // It would be thrown during resource streaming anyway which is a better moment.
      }
    }
  }

  /**
   * Returns true if the given object is also an instance of {@link CombinedResourceInfo} and its ID equals to the
   * ID of the current combined resource info instance.
   */
  @Override
  public boolean equals(Object other) {
    return (other instanceof CombinedResourceInfo)
      ? ((CombinedResourceInfo) other).id.equals(id)
      : false;
  }

  /**
   * Returns the sum of the hash code of this class and the ID.
   */
  @Override
  public int hashCode() {
    return getClass().hashCode() + id.hashCode();
  }

  /**
   * Returns the string representation of this combined resource info in the format of
   * <pre>CombinedResourceInfo[id,resourceIdentifiers]</pre>
   * Where <code>id</code> is the unique ID and <code>resourceIdentifiers</code> is the ordered set of all resource
   * identifiers as is been created with the builder.
   */
  @Override
  public String toString() {
    return String.format("CombinedResourceInfo[%s,%s]", id, resourceIdentifiers);
  }

  // Getters --------------------------------------------------------------------------------------------------------

  /**
   * Returns the ordered set of resource identifiers of this combined resource info.
   * @return the ordered set of resource identifiers of this combined resource info.
   */
  public Set<ResourceIdentifier> getResourceIdentifiers() {
    return resourceIdentifiers;
  }

  /**
   * Returns the ordered set of resources of this combined resource info.
   * @return The ordered set of resources of this combined resource info.
   */
  public Set<Resource> getResources() {
    loadResources();
    return resources;
  }

  /**
   * Returns the content length in bytes of this combined resource info.
   * @return The content length in bytes of this combined resource info.
   */
  public int getContentLength() {
    loadResources();
    return contentLength;
  }

  /**
   * Returns the last modified timestamp in milliseconds of this combined resource info.
   * @return The last modified timestamp in milliseconds of this combined resource info.
   */
  public long getLastModified() {
    loadResources();
    return lastModified;
  }

  // Helpers ----------------------------------------------------------------------------------------------------

  /**
   * Create an unique ID based on the given set of resource identifiers. The current implementation converts the
   * set to a <code>|</code>-delimited string which is serialized using {@link Utils#serialize(String)}.
   * @param resourceIdentifiers The set of resource identifiers to create an unique ID for.
   * @return The unique ID of the given set of resource identifiers.
   */
  private static String toUniqueId(Set<ResourceIdentifier> resourceIdentifiers) {
    return serializeURLSafe(Converters.joinCollection(resourceIdentifiers, "|"));
  }

  /**
   * Create an ordered set of resource identifiers based on the given unique ID. This does the reverse of
   * {@link #toUniqueId(Map)}.
   * @param id The unique ID of the set of resource identifiers.
   * @return The set of resource identifiers based on the given unique ID, or <code>null</code> if the ID is not
   * valid.
   */
  private static Set<ResourceIdentifier> fromUniqueId(String id) {
    String resourcesId;

    try {
      resourcesId = unserializeURLSafe(id);
    }
    catch (IllegalArgumentException e) {
      // This will occur when the ID has purposefully been manipulated for some reason.
      // Just return null then so that it will end up in a 404.
      return null;
    }

    Set<ResourceIdentifier> resourceIdentifiers = new LinkedHashSet<>();

    for (String resourceIdentifier : resourcesId.split("\\|")) {
      resourceIdentifiers.add(new ResourceIdentifier(resourceIdentifier));
    }

    return resourceIdentifiers;
  }

}
TOP

Related Classes of org.omnifaces.resourcehandler.CombinedResourceInfo$Builder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.