content: /*
   * Copyright (c) 2008. All rights reserved.
   */
  package ro.isdc.wro.model.factory;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import javax.xml.XMLConstants;
  import javax.xml.parsers.DocumentBuilderFactory;
  import javax.xml.transform.dom.DOMSource;
  import javax.xml.transform.stream.StreamSource;
  import javax.xml.validation.Schema;
  import javax.xml.validation.SchemaFactory;
  
  import org.apache.commons.io.input.AutoCloseInputStream;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.commons.lang3.Validate;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.w3c.dom.Document;
  import org.w3c.dom.Element;
  import org.w3c.dom.Node;
  import org.w3c.dom.NodeList;
  import org.xml.sax.SAXException;
  
  import ro.isdc.wro.WroRuntimeException;
  import ro.isdc.wro.model.WroModel;
  import ro.isdc.wro.model.WroModelInspector;
  import ro.isdc.wro.model.group.Group;
  import ro.isdc.wro.model.group.Inject;
  import ro.isdc.wro.model.group.RecursiveGroupDefinitionException;
  import ro.isdc.wro.model.group.processor.Injector;
  import ro.isdc.wro.model.resource.Resource;
  import ro.isdc.wro.model.resource.ResourceType;
  import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
  import ro.isdc.wro.util.StopWatch;
  
  
  /**
   * Model factory implementation. Creates a WroModel object, based on an xml. This xml contains the description of all
   * groups.
   * <p/>
   * This class is thread-safe (the create method is synchronized).
   *
   * @author Alex Objelean
   * @created Created on Nov 3, 2008
   */
  public class XmlModelFactory
      extends AbstractWroModelFactory {
    /**
     * Logger for this class.
     */
    private static final Logger LOG = LoggerFactory.getLogger(XmlModelFactory.class);
    /**
     * The alias for this model factory used by spi provider.
     */
    public static final String ALIAS = "xml";
    /**
     * Default xml filename.
     */
    private static final String DEFAULT_FILE_NAME = "wro.xml";
  
    /**
     * Default xml to parse.
     */
    private static final String XML_SCHEMA_FILE = "wro.xsd";
  
    /**
     * Group tag used in xml.
     */
    private static final String TAG_GROUP = "group";
    /**
     * Import tag used in xml.
     */
    private static final String TAG_IMPORT = "import";
    /**
     * CSS tag used in xml.
     */
    private static final String TAG_CSS = "css";
  
    /**
     * JS tag used in xml.
     */
    private static final String TAG_JS = "js";
  
    /**
     * GroupRef tag used in xml.
     */
    private static final String TAG_GROUP_REF = "group-ref";
  
    /**
     * Group name attribute.
     */
    private static final String ATTR_GROUP_NAME = "name";
    /**
     * Group abstract attribute used to distinguish an abstract group when its value is true. By default the value is
     * false.
     */
    private static final String ATTR_GROUP_ABSTRACT = "abstract";
    /**
     * Minimize attribute specified on resource level, used to turn on/off minimization on this particular resource during
     * pre processing.
     */
    private static final String ATTR_MINIMIZE = "minimize";
    /**
     * Map between the group name and corresponding element. Hold the map<GroupName, Element> of all group nodes to access
     * any element.
     */
    final Map<String, Element> allGroupElements = new HashMap<String, Element>();
  
    /**
     * List of groups which are currently being processing and are partially parsed. This list is useful in order to catch
     * infinite recurse group reference.
     */
    final Collection<String> groupsInProcess = new HashSet<String>();
  
    /**
     * Used to locate imports;
     */
    @Inject
    private UriLocatorFactory locatorFactory;
    @Inject
    private Injector injector;
    /**
     * Used to detect recursive import processing.
     */
    private final Set<String> processedImports = new HashSet<String>();
    /**
     * Flag for enabling xml validation.
     */
    private boolean validateXml = true;
    /**
     * The model being created.
     */
    private WroModel model;
  
    /**
     * Default constructor.
     */
    public XmlModelFactory() {
    }
  
    /**
     * Allow aggregate processed imports during recursive model creation.
     */
    private XmlModelFactory(final Set<String> processedImports) {
      Validate.notNull(processedImports);
      this.processedImports.addAll(processedImports);
    }
  
    /**
     * {@inheritDoc}
     */
    public synchronized WroModel create() {
      model = new WroModel();
      final StopWatch stopWatch = new StopWatch("Create Wro Model from XML");
      try {
        stopWatch.start("createDocument");
        final Document document = createDocument();
        stopWatch.stop();
  
        stopWatch.start("processGroups");
        processGroups(document);
        stopWatch.stop();
  
        stopWatch.start("processImports");
        processImports(document);
        stopWatch.stop();
  
        stopWatch.start("createModel");
        parseGroups();
        stopWatch.stop();
        return model;
      } finally {
        // clear the processed imports even when the model creation fails.
        processedImports.clear();
        LOG.debug(stopWatch.prettyPrint());
      }
    }
  
    /**
     * @return valid {@link Document} of the xml containing model representation.
     */
    private Document createDocument() {
      try {
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        final Document document = factory.newDocumentBuilder().parse(getModelResourceAsStream());
        document.getDocumentElement().normalize();
        if (isValidateXml()) {
          validate(document);
        }
        return document;
      } catch (final Exception e) {
        throw new WroRuntimeException("Cannot build model from XML: " + e.getMessage(), e);
      }
    }
  
    /**
     * @param document
     *          xml document to validate.
     */
    private void validate(final Document document)
        throws IOException, SAXException {
      final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
      final Schema schema = factory.newSchema(new StreamSource(getSchemaStream()));
      schema.newValidator().validate(new DOMSource(document));
    }
  
    private InputStream getSchemaStream()
        throws IOException {
      // use the class located in same package where xsd is located
      return WroRuntimeException.class.getResourceAsStream(XML_SCHEMA_FILE);
    }
  
    /**
     * Initialize the map
     */
    private void processGroups(final Document document) {
      // handle imports
      final NodeList groupNodeList = document.getElementsByTagName(TAG_GROUP);
      for (int i = 0; i < groupNodeList.getLength(); i++) {
        final Element groupElement = (Element) groupNodeList.item(i);
        final String name = groupElement.getAttribute(ATTR_GROUP_NAME);
        allGroupElements.put(name, groupElement);
      }
    }
  
    private void processImports(final Document document) {
      final NodeList importsList = document.getElementsByTagName(TAG_IMPORT);
      LOG.debug("number of imports: {}", importsList.getLength());
      for (int i = 0; i < importsList.getLength(); i++) {
        final Element element = (Element) importsList.item(i);
        final String name = element.getTextContent();
        LOG.debug("processing import: {}", name);
        Validate.notNull(locatorFactory, "The Locator cannot be null!");
        if (processedImports.contains(name)) {
          final String message = "Recursive import detected: " + name;
          LOG.error(message);
          throw new RecursiveGroupDefinitionException(message);
        }
  
        processedImports.add(name);
        model.merge(createImportedModel(name));
      }
    }
  
    /**
     * @param modelLocation
     *          the path where the model is located.
     * @return the {@link WroModel} created from provided modelLocation.
     */
    private WroModel createImportedModel(final String modelLocation) {
      final XmlModelFactory importedModelFactory = new XmlModelFactory(this.processedImports) {
        @Override
        protected InputStream getModelResourceAsStream()
            throws IOException {
          LOG.debug("build model from import: {}", modelLocation);
          return new AutoCloseInputStream(locatorFactory.locate(modelLocation));
        };
      };
      // inject manually created modelFactory
      injector.inject(importedModelFactory);
      try {
        return importedModelFactory.create();
      } catch (final WroRuntimeException e) {
        LOG.error("Detected invalid model import from location {}", modelLocation);
        throw e;
      }
    }
  
    /**
     * Parse the document and creates groups which are added to the provided model.
     *
     * @param document
     *          to parse.
     */
    private void parseGroups() {
      // use groups created by imports (if any)
      for (final Element element : allGroupElements.values()) {
        parseGroup(element);
      }
    }
  
    /**
     * Recursive method. Add the parsed element group to the group collection. If the group contains group-ref element,
     * parse recursively this group.
     *
     * @param element
     *          Group Element to parse.
     * @param groups
     *          list of parsed groups where the parsed group is added..
     * @return list of resources associated with this resource
     */
    private Collection<Resource> parseGroup(final Element element) {
      final String name = element.getAttribute(ATTR_GROUP_NAME);
      final String isAbstractAsString = element.getAttribute(ATTR_GROUP_ABSTRACT);
      final boolean isAbstractGroup = StringUtils.isNotEmpty(isAbstractAsString) && Boolean.valueOf(isAbstractAsString);
      if (groupsInProcess.contains(name)) {
        throw new RecursiveGroupDefinitionException("Infinite Recursion detected for the group: " + name
            + ". Recursion path: " + groupsInProcess);
      }
      LOG.debug("\tadding group: {}", name);
      groupsInProcess.add(name);
      // skip if this group is already parsed
      final Group parsedGroup = new WroModelInspector(model).getGroupByName(name);
      if (parsedGroup != null) {
        // remove before returning
        // this group is parsed, remove from unparsed groups collection
        groupsInProcess.remove(name);
        return parsedGroup.getResources();
      }
      final Group group = new Group(name);
      final List<Resource> resources = new ArrayList<Resource>();
      final NodeList resourceNodeList = element.getChildNodes();
      for (int i = 0; i < resourceNodeList.getLength(); i++) {
        final Node node = resourceNodeList.item(i);
        if (node instanceof Element) {
          final Element resourceElement = (Element) node;
          parseResource(resourceElement, resources);
        }
      }
      group.setResources(resources);
      // this group is parsed, remove from unparsed collection
      groupsInProcess.remove(name);
      if (!isAbstractGroup) {
        // add only non abstract groups
        model.addGroup(group);
      }
      return resources;
    }
  
    /**
     * Creates a resource from a given resourceElement. It can be css, js. If resource tag name is group-ref, the method
     * will start a recursive computation.
     *
     * @param resourceElement
     * @param resources
     *          list of parsed resources where the parsed resource is added.
     */
    private void parseResource(final Element resourceElement, final Collection<Resource> resources) {
      ResourceType type = null;
      final String tagName = resourceElement.getTagName();
      final String uri = resourceElement.getTextContent();
      if (TAG_JS.equals(tagName)) {
        type = ResourceType.JS;
      } else if (TAG_CSS.equals(tagName)) {
        type = ResourceType.CSS;
      } else if (TAG_GROUP_REF.equals(tagName)) {
        // uri in this case is the group name
        resources.addAll(getResourcesForGroup(uri));
      }
      if (type != null) {
        final String minimizeAsString = resourceElement.getAttribute(ATTR_MINIMIZE);
        final boolean minimize = StringUtils.isEmpty(minimizeAsString) ? true
            : Boolean.valueOf(resourceElement.getAttribute(ATTR_MINIMIZE));
        final Resource resource = Resource.create(uri, type);
        resource.setMinimize(minimize);
        LOG.debug("\t\tadding resource: {}", resource);
        resources.add(resource);
      }
    }
  
    /**
     * Search for all resources for a group with a given name.
     */
    private Collection<Resource> getResourcesForGroup(final String groupName) {
      final WroModelInspector modelInspector = new WroModelInspector(model);
      final Group foundGroup = modelInspector.getGroupByName(groupName);
      if (foundGroup == null) {
        Collection<Resource> groupResources = null;
        final Element groupElement = allGroupElements.get(groupName);
        if (groupElement == null) {
          throw new WroRuntimeException("Invalid group-ref: " + groupName);
        }
        groupResources = parseGroup(groupElement);
        return groupResources;
      }
      return foundGroup.getResources();
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    protected String getDefaultModelFilename() {
      return DEFAULT_FILE_NAME;
    }
  
    /**
     * @return true if xml validation should be performed.
     */
    private boolean isValidateXml() {
      return this.validateXml;
    }
  
    /**
     * Allows disable the xml validation (which is true by default. Be aware that this is not a good idea to disable
     * validation, because you cannot be sure that the model is built correctly. Disabling makes sense only when for some
     * reason the xsd schema cannot be loaded on some environments. An example of issue can be found here:<br/>
     * <a href="http://code.google.com/p/wro4j/issues/detail?id=371">Wro4j doesn't work on Websphere with 2 or more
     * wars</a>
     */
    public XmlModelFactory setValidateXml(final boolean validateXml) {
      this.validateXml = validateXml;
      return this;
    }
  }
  