content: /*
   * Copyright (c) 2008. All rights reserved.
   */
  package ro.isdc.wro.model.group;
  
  import static org.apache.commons.lang3.Validate.notNull;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Set;
  
  import org.apache.commons.lang3.Validate;
  import org.apache.commons.lang3.builder.ToStringBuilder;
  import org.apache.commons.lang3.builder.ToStringStyle;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import ro.isdc.wro.model.resource.Resource;
  import ro.isdc.wro.model.resource.ResourceType;
  
  
  /**
   * A group is an entity holding a list of resources.
   * <p/>
   * This class is thread safe.
   *
   * @author Alex Objelean
   * @created Created on Oct 30, 2008
   */
  public final class Group {
    private static final Logger LOG = LoggerFactory.getLogger(Group.class);
    /**
     * Group name.
     */
    private String name;
  
    /**
     * Resources of the group.
     */
    private final List<Resource> resources = Collections.synchronizedList(new ArrayList<Resource>());
  
    /**
     * To be used by JSON serializer.
     */
    public Group() {
    }
  
    /**
     * Creates a group with a name.
     *
     * @param name
     *          of the group.
     */
    public Group(final String name) {
      Validate.notNull(name, "Group name cannot be null.");
      this.name = name;
    }
  
    /**
     * Check if the group has at least one resource of some type.
     *
     * @param resourceType
     *          type of the searched resource.
     * @return true if at least one resource of some type exists.
     */
    public final boolean hasResourcesOfType(final ResourceType resourceType) {
      Validate.notNull(resourceType, "ResourceType cannot be null!");
      for (final Resource resource : resources) {
        if (resourceType.equals(resource.getType())) {
          return true;
        }
      }
      return false;
    }
  
    /**
     * @return true if the resourceToCheck is already contained in this group.
     */
    private boolean hasResource(final Resource resourceToCheck) {
      for (final Resource resource : resources) {
        if (resource.equals(resourceToCheck)) {
          return true;
        }
      }
      return false;
    }
  
    /**
     * @return true if a {@link Resource} with an uri same as resourceUri is contained in this group.
     */
    public boolean hasResource(final String resourceUri) {
      for (final Resource resource : resources) {
        if (resource.getUri().equals(resourceUri)) {
          return true;
        }
      }
      return false;
    }
  
    /**
     * Replace one resource with a list of other resources. The use case is related to wildcard expander functionality,
     * when resources containing wildcard are replaced with a list of wildcard-free resources. The order of resources is
     * preserved.
     * <p/>
     * The implementation is synchronized, because it mutates the collection.
     *
     * @param resource
     *          to replace.
     * @param expandedResources
     *          a list of resources to replace. If this list is empty, the result is similar to removing the resource from
     *          the group.
     * @throws IllegalArgumentException
     *           when a missing resources is to be replaced.
     */
    public void replace(final Resource resource, final List<Resource> expandedResources) {
      LOG.debug("replacing resource {} with expanded resources: {}", resource, expandedResources);
      Validate.notNull(resource);
      Validate.notNull(expandedResources);
      synchronized (this) {
        boolean found = false;
        // use set to avoid duplicates
        final Set<Resource> result = new LinkedHashSet<Resource>();
        for (final Resource resourceItem : resources) {
          if (resourceItem.equals(resource)) {
            found = true;
            for (final Resource expandedResource : expandedResources) {
              // preserve minimize flag.
              expandedResource.setMinimize(resource.isMinimize());
              result.add(expandedResource);
            }
          } else {
            result.add(resourceItem);
          }
        }
        // if no resources found, an invalid replace is performed
        if (!found) {
          throw new IllegalArgumentException("Cannot replace resource: " + resource + " for group: " + this
              + " because the resource is not a part of this group.");
        }
        // update resources with newly built list.
        setResources(new ArrayList<Resource>(result));
      }
    }
  
    /**
     * @param type
     *          of resources to collect. This value should not be null.
     * @return a new group with the same name as original containing filtered resources. The created group has the same
     *         name.
     */
    public final Group collectResourcesOfType(final ResourceType type) {
      Validate.notNull(type);
      final List<Resource> allResources = new ArrayList<Resource>();
      allResources.addAll(getResources());
  
      // retain only resources of needed type
      final List<Resource> filteredResources = new ArrayList<Resource>();
      for (final Resource resource : getResources()) {
        if (type == resource.getType()) {
          if (filteredResources.contains(resource)) {
            LOG.warn("Duplicated resource detected: " + resource + ". This resource won't be included more than once!");
          } else {
            filteredResources.add(resource);
          }
        }
      }
  
      final Group filteredGroup = new Group(getName());
      filteredGroup.setResources(filteredResources);
      return filteredGroup;
    }
  
    /**
     * @return the readonly list of resources.
     */
    public List<Resource> getResources() {
      // use a new list to avoid ConcurrentModificationException when the Group#replace method is called.
      return Collections.unmodifiableList(new ArrayList<Resource>(resources));
    }
  
    /**
     * Add a {@link Resource} to the collection of resources associated with this group.
     * <p/>
     * The implementation is synchronized, because it mutates the collection.
     *
     * @param resource {@link Resource} to add to this group (at the end).
     * @return the reference to Group (fluent interface).
     */
    public Group addResource(final Resource resource) {
      notNull(resource);
      synchronized (this) {
        if (!hasResource(resource)) {
          resources.add(resource);
        } else {
          LOG.debug("Resource {} is already contained in this group, skipping it.", resource);
        }
      }
      return this;
    }
  
    /**
     * This method will replace all earlier defined resources with the provided list of resources.
     * <p/>
     * The implementation is synchronized, because it mutates the collection.
     *
     * @param resources
     *          the resources to replace the underlying resources.
     */
    public final void setResources(final List<Resource> resources) {
      notNull(resources);
      synchronized (this) {
        this.resources.clear();
        for (final Resource resource : resources) {
          addResource(resource);
        }
      }
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(final Object obj) {
      // Because name is unique, we can consider two groups are equals if their name is the same
      if (obj instanceof Group) {
        final Group group = (Group) obj;
        return getName().equals(group.getName());
      }
      return false;
    }
  
    /**
     * @return the name
     */
    public String getName() {
      return name;
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
      return getName().hashCode();
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
      return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }
  }
  