/*
  Copyright 2011-2014 Red Hat, Inc

  This file is part of PressGang CCMS.

  PressGang CCMS is free software: you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  PressGang CCMS is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with PressGang CCMS.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.jboss.pressgang.ccms.model.contentspec;

import static javax.persistence.GenerationType.IDENTITY;

import javax.persistence.Cacheable;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.Audited;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.hibernate.validator.constraints.NotBlank;
import org.jboss.pressgang.ccms.model.PropertyTag;
import org.jboss.pressgang.ccms.model.Topic;
import org.jboss.pressgang.ccms.model.base.ParentToPropertyTag;
import org.jboss.pressgang.ccms.model.constants.Constants;
import org.jboss.pressgang.ccms.model.exceptions.CustomConstraintViolationException;
import org.jboss.pressgang.ccms.model.interfaces.HasCSNodes;
import org.jboss.pressgang.ccms.model.interfaces.HasTwoWayRelationships;
import org.jboss.pressgang.ccms.model.utils.EnversUtilities;
import org.jboss.pressgang.ccms.utils.constants.CommonConstants;

@Entity
@Audited
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Table(name = "ContentSpecNode")
public class CSNode extends ParentToPropertyTag<CSNode, CSNodeToPropertyTag> implements HasCSNodes, HasTwoWayRelationships<CSNodeToCSNode>,
        Serializable {

    private static final long serialVersionUID = -5074781793940947664L;
    public static final String SELECT_ALL_QUERY = "select csNode FROM CSNode AS csNode";

    private Integer csNodeId = null;
    private String csNodeTitle = null;
    private String csNodeTargetId = null;
    private String additionalText = null;
    private Integer csNodeType = null;
    private ContentSpec contentSpec = null;
    private CSNode parent = null;
    private CSNode next = null;
    private CSNode previous = null;
    private Integer entityId = null;
    private Integer entityRevision = null;
    private String condition = null;
    private Set<CSNode> children = new HashSet<CSNode>(0);
    private Set<CSNodeToCSNode> relatedFromNodes = new HashSet<CSNodeToCSNode>(0);
    private Set<CSNodeToCSNode> relatedToNodes = new HashSet<CSNodeToCSNode>(0);
    private Set<CSNodeToPropertyTag> csNodeToPropertyTags = new HashSet<CSNodeToPropertyTag>(0);
    private CSInfoNode csInfoNode;
    private CSNodeURL csNodeURL;

    private Topic topic;

    public CSNode() {
    }

    @Override
    @Transient
    public Integer getId() {
        return csNodeId;
    }

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "ContentSpecNodeID", unique = true, nullable = false)
    public Integer getCSNodeId() {
        return csNodeId;
    }

    public void setCSNodeId(Integer csNodeId) {
        this.csNodeId = csNodeId;
    }

    @Column(name = "NodeTitle", length = 255)
    @NotNull(message = "{contentspec.node.title.notBlank}")
    @NotBlank(message = "{contentspec.node.title.notBlank}")
    public String getCSNodeTitle() {
        return csNodeTitle;
    }

    public void setCSNodeTitle(String csNodeTitle) {
        this.csNodeTitle = csNodeTitle;
    }

    @Column(name = "NodeTargetID", length = 255, nullable = true)
    public String getCSNodeTargetId() {
        return csNodeTargetId;
    }

    public void setCSNodeTargetId(String csNodeTargetId) {
        this.csNodeTargetId = csNodeTargetId;
    }

    @Column(name = "AdditionalText", columnDefinition = "TEXT", nullable = true)
    @Size(max = 65535)
    public String getAdditionalText() {
        return additionalText;
    }

    public void setAdditionalText(String csNodeAlternativeTitle) {
        this.additionalText = csNodeAlternativeTitle;
    }

    @Column(name = "NodeType", nullable = false, columnDefinition = "TINYINT")
    public Integer getCSNodeType() {
        return csNodeType;
    }

    public void setCSNodeType(Integer csNodeType) {
        this.csNodeType = csNodeType;
    }

    @JoinColumn(name = "ContentSpecID")
    @ManyToOne(fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    public ContentSpec getContentSpec() {
        return contentSpec;
    }

    public void setContentSpec(ContentSpec contentSpec) {
        this.contentSpec = contentSpec;

        for (final CSNode child : children) {
            child.setContentSpec(contentSpec);
        }

        if (csNodeURL != null) {
            csNodeURL.setContentSpec(contentSpec);
        }
    }

    @JoinColumn(name = "ParentID")
    @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    public CSNode getParent() {
        return parent;
    }

    public void setParent(CSNode parent) {
        this.parent = parent;
    }

    @JoinColumn(name = "NextNodeID")
    @OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    public CSNode getNext() {
        return next;
    }

    public void setNext(CSNode next) {
        this.next = next;
    }

    /**
     * Sets the Next Node and cleans up any old references.
     *
     * @param next The next node.
     */
    @Transient
    public void setNextAndClean(CSNode next) {
        setNextInternal(next);
        if (next != null) {
            next.setPreviousInternal(this);
        }
    }

    @Transient
    protected void setNextInternal(CSNode next) {
        if (this.next != next) {
            if (this.next != null) {
                this.next.previous = null;
            }
            this.next = next;
        }
    }

    @OneToOne(fetch = FetchType.LAZY, mappedBy = "next", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    public CSNode getPrevious() {
        return previous;
    }

    public void setPrevious(CSNode previous) {
        this.previous = previous;
    }

    @OneToOne(mappedBy = "CSNode", cascade = CascadeType.ALL, orphanRemoval = true)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    public CSInfoNode getCSInfoNode() {
        return csInfoNode;
    }

    public void setCSInfoNode(CSInfoNode csInfoNode) {
        this.csInfoNode = csInfoNode;
    }

    @OneToOne(mappedBy = "CSNode", cascade = CascadeType.ALL, orphanRemoval = true, optional = true)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    public CSNodeURL getCSNodeURL() {
        return csNodeURL;
    }

    public void setCSNodeURL(CSNodeURL csNodeURL) {
        this.csNodeURL = csNodeURL;
    }

    /**
     * Sets the Previous Node and cleans up any old references.
     *
     * @param previous The previous node.
     */
    @Transient
    public void setPreviousAndClean(CSNode previous) {
        setPreviousInternal(previous);
        if (previous != null) {
            previous.setNextInternal(this);
        }
    }

    @Transient
    protected void setPreviousInternal(CSNode previous) {
        if (this.previous != previous) {
            if (this.previous != null) {
                this.previous.next = null;
            }
            this.previous = previous;
        }
    }

    @Column(name = "EntityID", nullable = true)
    public Integer getEntityId() {
        return entityId;
    }

    public void setEntityId(Integer entityId) {
        this.entityId = entityId;
    }

    @Column(name = "EntityRevision", nullable = true)
    public Integer getEntityRevision() {
        return entityRevision;
    }

    public void setEntityRevision(Integer entityRevision) {
        this.entityRevision = entityRevision;
    }

    @Column(name = "NodeCondition", nullable = true)
    @Size(max = 255)
    public String getCondition() {
        return condition;
    }

    public void setCondition(String condition) {
        this.condition = condition;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    @Override
    public Set<CSNode> getChildren() {
        return children;
    }

    public void setChildren(Set<CSNode> children) {
        this.children = children;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "mainNode", cascade = CascadeType.ALL, orphanRemoval = true)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<CSNodeToCSNode> getRelatedToNodes() {
        return relatedToNodes;
    }

    public void setRelatedFromNodes(Set<CSNodeToCSNode> relatedFromNodes) {
        this.relatedFromNodes = relatedFromNodes;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "relatedNode", cascade = CascadeType.ALL, orphanRemoval = true)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<CSNodeToCSNode> getRelatedFromNodes() {
        return relatedFromNodes;
    }

    public void setRelatedToNodes(Set<CSNodeToCSNode> relatedToNodes) {
        this.relatedToNodes = relatedToNodes;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "CSNode", cascade = CascadeType.ALL, orphanRemoval = true)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<CSNodeToPropertyTag> getCSNodeToPropertyTags() {
        return csNodeToPropertyTags;
    }

    public void setCSNodeToPropertyTags(Set<CSNodeToPropertyTag> csNodeToPropertyTags) {
        this.csNodeToPropertyTags = csNodeToPropertyTags;
    }

    @Transient
    @Override
    public List<CSNode> getChildrenList() {
        return new ArrayList<CSNode>(getChildren());
    }

    @Transient
    public List<CSNodeToCSNode> getRelatedFromNodesList() {
        return new ArrayList<CSNodeToCSNode>(relatedFromNodes);
    }

    @Transient
    public List<CSNodeToCSNode> getRelatedToNodesList() {
        return new ArrayList<CSNodeToCSNode>(relatedToNodes);
    }

    @Transient
    @Override
    public void removeChild(final CSNode child) {
        children.remove(child);
        child.setParent(null);
        if (contentSpec != null) {
            contentSpec.removeChild(child);
        }
    }

    @Transient
    @Override
    public void addChild(final CSNode child) {
        if (child.getParent() != null && !child.getParent().equals(this)) {
            child.getParent().removeChild(child);
        }
        if (child.getContentSpec() != null) {
            child.getContentSpec().removeChild(child);
        }
        children.add(child);
        child.setParent(this);
        if (contentSpec != null) {
            contentSpec.addChild(child);
        }
    }

    @Transient
    public void removeRelationshipTo(final CSNode node, Integer relationshipTypeId) {
        final List<CSNodeToCSNode> removeNodes = new ArrayList<CSNodeToCSNode>();

        for (final CSNodeToCSNode nodeToNode : relatedToNodes) {
            if (nodeToNode.getRelatedNode().equals(node) && nodeToNode.getRelationshipType().equals(relationshipTypeId)) {
                removeNodes.add(nodeToNode);
            }
        }

        for (final CSNodeToCSNode removeNode : removeNodes) {
            removeRelationshipTo(removeNode);
        }
    }

    @Transient
    public void addRelationshipTo(final CSNode relatedNode, Integer relationshipTypeId) {
        final CSNodeToCSNode nodeToNode = new CSNodeToCSNode();
        nodeToNode.setMainNode(this);
        nodeToNode.setRelatedNode(relatedNode);
        nodeToNode.setRelationshipType(relationshipTypeId);

        addRelationshipTo(nodeToNode);
    }

    @Transient
    @Override
    public void addRelationshipTo(final CSNodeToCSNode relatedNode) {
        relatedNode.setMainNode(this);
        getRelatedToNodes().add(relatedNode);
        relatedNode.getRelatedNode().getRelatedFromNodes().add(relatedNode);
    }

    @Transient
    @Override
    public void removeRelationshipTo(final CSNodeToCSNode relatedNode) {
        getRelatedToNodes().remove(relatedNode);
        relatedNode.getRelatedNode().getRelatedFromNodes().remove(relatedNode);
    }

    @Transient
    public void removeRelationshipFrom(final CSNode node, Integer relationshipTypeId) {
        final List<CSNodeToCSNode> removeNodes = new ArrayList<CSNodeToCSNode>();

        for (final CSNodeToCSNode nodeFromNode : relatedFromNodes) {
            if (nodeFromNode.getRelatedNode().equals(node) && nodeFromNode.getRelationshipType().equals(relationshipTypeId)) {
                removeNodes.add(nodeFromNode);
            }
        }

        for (final CSNodeToCSNode removeNode : removeNodes) {
            removeRelationshipFrom(removeNode);
        }
    }

    @Transient
    public void addRelationshipFrom(final CSNode relatedNode, Integer relationshipTypeId) {
        final CSNodeToCSNode nodeFromNode = new CSNodeToCSNode();
        nodeFromNode.setMainNode(relatedNode);
        nodeFromNode.setRelatedNode(this);
        nodeFromNode.setRelationshipType(relationshipTypeId);

        addRelationshipFrom(nodeFromNode);
    }

    @Transient
    @Override
    public void addRelationshipFrom(final CSNodeToCSNode relatedNode) {
        relatedNode.setRelatedNode(this);
        getRelatedFromNodes().add(relatedNode);
        relatedNode.getRelatedNode().getRelatedToNodes().add(relatedNode);
    }

    @Transient
    @Override
    public void removeRelationshipFrom(final CSNodeToCSNode relatedNode) {
        getRelatedFromNodes().remove(relatedNode);
        relatedNode.getMainNode().getRelatedToNodes().remove(relatedNode);
    }

    @Transient
    @Override
    public Set<CSNodeToPropertyTag> getPropertyTags() {
        return csNodeToPropertyTags;
    }

    @Override
    public void addPropertyTag(final PropertyTag propertyTag, final String value) {
        final CSNodeToPropertyTag mapping = new CSNodeToPropertyTag();
        mapping.setCSNode(this);
        mapping.setPropertyTag(propertyTag);
        mapping.setValue(value);

        csNodeToPropertyTags.add(mapping);
        propertyTag.getCSNodeToPropertyTags().add(mapping);
    }

    @Override
    public void addPropertyTag(final CSNodeToPropertyTag mapping) {
        mapping.setCSNode(this);
        csNodeToPropertyTags.add(mapping);
        mapping.getPropertyTag().getCSNodeToPropertyTags().add(mapping);
    }

    @Override
    public void removePropertyTag(final PropertyTag propertyTag, final String value) {
        final List<CSNodeToPropertyTag> removeList = new ArrayList<CSNodeToPropertyTag>();

        for (final CSNodeToPropertyTag mapping : csNodeToPropertyTags) {
            final PropertyTag myPropertyTag = mapping.getPropertyTag();
            if (myPropertyTag.equals(propertyTag) && mapping.getValue().equals(value)) {
                removeList.add(mapping);
            }
        }

        for (final CSNodeToPropertyTag mapping : removeList) {
            removePropertyTag(mapping);
        }
    }

    @Override
    public void removePropertyTag(final CSNodeToPropertyTag mapping) {
        csNodeToPropertyTags.remove(mapping);
        mapping.getPropertyTag().getCSNodeToPropertyTags().remove(mapping);
    }

    @Transient
    public Topic getTopic(final EntityManager entityManager) {
        if (!isTopicNode())
            return null;
        if (entityId == null)
            return null;

        if (topic == null) {
            if (entityRevision == null) {
                // Find the latest topic
                topic = entityManager.find(Topic.class, getEntityId());
            } else {
                // Find the envers topic
                topic = EnversUtilities.getRevision(entityManager, Topic.class, getEntityId(), getEntityRevision());
            }
        }
        return topic;
    }

    @SuppressWarnings("unchecked")
    @Transient
    public List<TranslatedCSNode> getTranslatedNodes(final EntityManager entityManager, final Number revision) {
        /*
         * We have to do a query here as a @OneToMany won't work with hibernate envers since the TranslatedCSNode entity is
         * audited and we need the latest results. This is because the translated node will never exist for its matching
         * audited node.
         */
        final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        final CriteriaQuery<TranslatedCSNode> query = criteriaBuilder.createQuery(TranslatedCSNode.class);
        final Root<TranslatedCSNode> root = query.from(TranslatedCSNode.class);
        query.select(root);

        final Predicate csNodeIdMatches = criteriaBuilder.equal(root.get("CSNodeId"), csNodeId);
        final Predicate csNodeRevisionMatches = criteriaBuilder.lessThanOrEqualTo(root.get("CSNodeRevision").as(Integer.class),
                (Integer) revision);

        if (revision == null) {
            query.where(csNodeIdMatches);
        } else {
            query.where(criteriaBuilder.and(csNodeIdMatches, csNodeRevisionMatches));
        }

        return entityManager.createQuery(query).getResultList();
    }

    @Transient
    public String getInheritedCondition() {
        if (getCondition() == null || getCondition().trim().isEmpty()) {
            if (getParent() != null) {
                return getParent().getInheritedCondition();
            } else if (getContentSpec() != null) {
                return getContentSpec().getCondition();
            } else {
                return null;
            }
        } else {
            return getCondition();
        }
    }

    @Transient
    public void setFixedUrl(final String url) {
        if (csNodeURL == null) {
            csNodeURL = new CSNodeURL();
            csNodeURL.setCSNode(this);
            csNodeURL.setContentSpec(contentSpec);
        }

        csNodeURL.setUrl(url);
    }

    @PrePersist
    @PreUpdate
    protected void preSave() {
        validateNode();

        // Set the content specs last modified date if one of it's nodes change
        if (contentSpec != null) {
            contentSpec.setLastModified();
        }
    }

    @Transient
    protected void validateNode() {
        if (getCSNodeType() == CommonConstants.CS_NODE_META_DATA && getParent() != null) {
            throw new CustomConstraintViolationException("Meta Data nodes are only allowed at the root level.");
        }

        if (isTopicNode() && !getChildren().isEmpty()) {
            throw new CustomConstraintViolationException("Topic nodes cannot have children nodes.");
        }
    }

    @PreRemove
    protected void preRemove() {
        if (next != null && previous != null) {
            next.previous = previous;
            previous.next = next;

            next = null;
            previous = null;
        } else if (next != null) {
            next.previous = null;
            next = null;
        } else if (previous != null) {
            previous.next = null;
            previous = null;
        }

        for (final CSNodeToCSNode mapping : new HashSet<CSNodeToCSNode>(relatedFromNodes)) {
            removeRelationshipFrom(mapping);
        }

        for (final CSNodeToCSNode mapping : new HashSet<CSNodeToCSNode>(relatedToNodes)) {
            removeRelationshipTo(mapping);
        }
    }

    @Transient
    public boolean isTopicNode() {
        return getCSNodeType() != null && (getCSNodeType().equals(CommonConstants.CS_NODE_TOPIC) || getCSNodeType().equals
                (CommonConstants.CS_NODE_INITIAL_CONTENT_TOPIC) || getCSNodeType().equals(CommonConstants.CS_NODE_META_DATA_TOPIC));
    }
}


/*
  Copyright 2011-2014 Red Hat, Inc

  This file is part of PressGang CCMS.

  PressGang CCMS is free software: you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  PressGang CCMS is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with PressGang CCMS.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.jboss.pressgang.ccms.model.utils;

import javax.persistence.EntityManager;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.query.AuditEntity;
import org.jboss.pressgang.ccms.model.base.AuditedEntity;

public class EnversUtilities {

    /**
     * When returning a collection of entity revisions, the lastModified property is set automatically (in the getRevision
     * method). For entities returned from a database query, the last modified date needs to be found manually.
     *
     * @return either the date saved in the lastModified property, or the latest revision date if lastModified is null
     */
    public static <T extends AuditedEntity> Date getFixedLastModifiedDate(final EntityManager entityManager, final T entity) {
        return entity.getLastModifiedDate() != null ? entity.getLastModifiedDate() : getLatestRevisionDate(entityManager, entity);
    }

    /**
     * @return Returns the latest Envers revision number
     */
    public static <T extends AuditedEntity> Date getLatestRevisionDate(final EntityManager entityManager, final T entity) {
        final AuditReader reader = AuditReaderFactory.get(entityManager);
        return reader.getRevisionDate(getLatestRevision(entityManager, entity));
    }

    /**
     * @return Returns a collection of revisions
     */
    public static <T extends AuditedEntity> Map<Number, T> getRevisionEntities(final EntityManager entityManager, final T entity) {
        return getRevisionEntities(entityManager, (Class<T>) entity.getClass(), entity.getId());
    }

    /**
     * @return Returns a collection of revisions
     */
    public static <T extends AuditedEntity> Map<Number, T> getRevisionEntities(final EntityManager entityManager,
            final Class<T> entityClass, final Integer id) {
        final AuditReader reader = AuditReaderFactory.get(entityManager);
        final List<Number> revisions = reader.getRevisions(entityClass, id);
        Collections.sort(revisions, Collections.reverseOrder());

        /* Use a LinkedHashMap to preserver the order */
        final Map<Number, T> retValue = new LinkedHashMap<Number, T>();
        for (final Number revision : revisions)
            retValue.put(revision, getRevision(reader, entityClass, id, revision, false));

        return retValue;
    }

    /**
     * @return Returns the list of revision numbers for this entity, as maintained by Envers
     */
    public static <T extends AuditedEntity> List<Number> getRevisions(final EntityManager entityManager, final T entity) {
        return getRevisions(entityManager, entity.getClass(), entity.getId());
    }

    /**
     * @return Returns the list of revision numbers for this entity, as maintained by Envers
     */
    public static <T extends AuditedEntity> List<Number> getRevisions(final EntityManager entityManager, final Class<T> entityClass,
            final Number id) {
        final AuditReader reader = AuditReaderFactory.get(entityManager);
        final List<Number> retValue = reader.getRevisions(entityClass, id);
        Collections.sort(retValue, Collections.reverseOrder());
        return retValue;
    }

    /**
     * @param entityManager
     * @param revision
     * @return
     */
    public static <T extends AuditedEntity> T getRevision(final EntityManager entityManager, final T entity, final Number revision) {
        final AuditReader reader = AuditReaderFactory.get(entityManager);
        return getRevision(reader, (Class<T>) entity.getClass(), entity.getId(), revision, true);
    }

    /**
     * @param entityManager
     * @param revision
     * @return
     */
    public static <T extends AuditedEntity> T getRevision(final EntityManager entityManager, final Class<T> entityClass, final Integer id,
            final Number revision) {
        final AuditReader reader = AuditReaderFactory.get(entityManager);
        return getRevision(reader, entityClass, id, revision, true);
    }

    @SuppressWarnings("unchecked")
    private static <T extends AuditedEntity> T getRevision(final AuditReader reader, final Class<T> entityClass, final Integer id,
            final Number revision, boolean revisionCheck) {
        final T revEntity = (T) reader.find(entityClass, id, revision);
        if (revEntity == null) return null;

        final Date revisionLastModified = reader.getRevisionDate(revision);
        revEntity.setLastModifiedDate(revisionLastModified);

        revEntity.setRevision(revisionCheck ? getClosestRevision(reader, entityClass, id, revision) : revision);

        return revEntity;
    }

    public static <T extends AuditedEntity> Number getLatestRevision(final EntityManager entityManager, final T entity) {
        return getLatestRevision(entityManager, entity.getClass(), entity.getId());
    }

    public static <T extends AuditedEntity> Number getLatestRevision(final EntityManager entityManager, final Class<T> entityClass,
            final Integer id) {
        final AuditReader reader = AuditReaderFactory.get(entityManager);
        final List<Number> retValue = reader.getRevisions(entityClass, id);
        Collections.sort(retValue, Collections.reverseOrder());
        return retValue.size() != 0 ? retValue.get(0) : -1;
    }

    public static <T extends AuditedEntity> Number getClosestRevision(final EntityManager entityManager, final T entity,
            final Number revision) {
        return getClosestRevision(entityManager, entity.getClass(), entity.getId(), revision);
    }

    public static <T extends AuditedEntity> Number getClosestRevision(final EntityManager entityManager, final Class<T> entityClass,
            final Integer id, final Number revision) {
        final AuditReader reader = AuditReaderFactory.get(entityManager);
        return getClosestRevision(reader, entityClass, id, revision);
    }

    public static <T extends AuditedEntity> Number getClosestRevision(final AuditReader reader, final Class<T> entityClass,
            final Integer id, final Number revision) {
        // Find the closest revision that is less than or equal to the revision specified.
        final Number closestRevision = (Number) reader.createQuery().forRevisionsOfEntity(entityClass, false, true).addProjection(
                AuditEntity.revisionNumber().max()).add(AuditEntity.id().eq(id)).add(
                AuditEntity.revisionNumber().le(revision)).getSingleResult();
        return closestRevision;
    }
}


/*
 * Icegem, Extensions library for VMWare vFabric GemFire
 *
 * Copyright (c) 2010-2011, Grid Dynamics Consulting Services Inc. or third-party
 * contributors as indicated by the @author tags or express copyright attribution
 * statements applied by the authors.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License v3, as published by the Free Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * You should have received a copy of the GNU Lesser General Public License v3
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package com.googlecode.icegem.cacheutils.monitor.controller.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.googlecode.icegem.cacheutils.common.Utils;
import com.googlecode.icegem.cacheutils.monitor.controller.event.NodeEvent;
import com.googlecode.icegem.cacheutils.monitor.controller.event.NodeEventHandler;
import com.googlecode.icegem.cacheutils.monitor.controller.event.NodeEventType;

/**
 * Container for the detected nodes.
 */
public class NodesContainer {
	private Map<String, Node> socketToNodeMap = new HashMap<String, Node>();
	private List<NodeEventHandler> handlers;

	public NodesContainer() {
		handlers = new ArrayList<NodeEventHandler>();
	}

	/**
	 * Finds the node by host and port.
	 *
	 * @param host
	 *            - the host.
	 * @param port
	 *            - the port.
	 * @return - the found node, or null otherwise.
	 */
	public Node find(String host, int port) {
		return socketToNodeMap.get(Utils.toKey(host, port));
	}

	/**
	 * Finds the set of nodes by host.
	 *
	 * @param host
	 *            - the host.
	 * @return - the found nodes.
	 */
	public Set<Node> find(String host) {
		Set<Node> resultSet = new HashSet<Node>();

		if (host != null) {
			for (Node node : socketToNodeMap.values()) {
				if (host.equals(node.getHost())) {
					resultSet.add(node);
				}
			}
		}

		return resultSet;
	}

	/**
	 * Gets all the nodes.
	 *
	 * @return - all the nodes.
	 */
	public Set<Node> getAll() {
		return new HashSet<Node>(socketToNodeMap.values());
	}

	/**
	 * Gets all the nodes not in status DEAD.
	 *
	 * @return - all the nodes not in status DEAD.
	 */
	public Set<Node> getAllNotDead() {
		Set<Node> allNotDeadNodesSet = new HashSet<Node>();

		for (Node node : socketToNodeMap.values()) {
			if (!NodeStatus.DEAD.equals(node.getStatus())) {
				allNotDeadNodesSet.add(node);
			}
		}

		return allNotDeadNodesSet;
	}

	/**
	 * Gets all the nodes in status DEAD.
	 *
	 * @return - all the nodes in status DEAD.
	 */
	public Set<Node> getAllDead() {
		Set<Node> allNotDeadNodesSet = new HashSet<Node>();

		for (Node node : socketToNodeMap.values()) {
			if (NodeStatus.DEAD.equals(node.getStatus())) {
				allNotDeadNodesSet.add(node);
			}
		}

		return allNotDeadNodesSet;
	}

	/**
	 * Adds one more node to the container.
	 *
	 * @param node
	 *            - the node.
	 */
	public void add(Node node) {
		socketToNodeMap.put(Utils.toKey(node), node);
		sendEvent(node, NodeEventType.ADDED);
	}

	/**
	 * Removes the specified node.
	 *
	 * @param node
	 *            - the node.
	 */
	public void remove(Node node) {
		socketToNodeMap.remove(Utils.toKey(node));
		sendEvent(node, NodeEventType.REMOVED);
	}

	/**
	 * Marks the node as ALIVE in case of its status is not ALIVE.
	 *
	 * @param node
	 *            - the node.
	 */
	public void markAsAlive(Node node) {
		if (!NodeStatus.ALIVE.equals(node.getStatus())) {
			socketToNodeMap.get(Utils.toKey(node)).markAsAlive();
			sendEvent(node, NodeEventType.MARKED_AS_ALIVE);
		}
	}

	/**
	 * Marks the node as DEAD in case of its status is not DEAD.
	 *
	 * @param node
	 *            - the node.
	 */
	public void markAsDead(Node node) {
		if (!NodeStatus.DEAD.equals(node.getStatus())) {
			socketToNodeMap.get(Utils.toKey(node)).markAsDead();
			sendEvent(node, NodeEventType.MARKED_AS_DEAD);
		}
	}

	/**
	 * Adds one more instance of NodeEventHandler to the list of event handlers.
	 *
	 * @param handler
	 *            - the handler.
	 */
	public void addNodeEventHandler(NodeEventHandler handler) {
		handlers.add(handler);
	}

	/**
	 * Sends the event to all the handlers.
	 *
	 * @param node
	 *            - the event node.
	 * @param type
	 *            - the event type.
	 */
	private void sendEvent(Node node, NodeEventType type) {
		NodeEvent event = new NodeEvent(node, type);
		for (NodeEventHandler handler : handlers) {
			handler.handle(event);
		}
	}

}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-visualization-api
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.visualization.api.factory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jguru.nazgul.tools.visualization.model.diagram.Graph;
import se.jguru.nazgul.tools.visualization.model.diagram.statement.Statement;

import java.util.Comparator;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Abstract TypeConverter implementation handling conversion to a {@link Graph}, and sporting
 * a small registry of {@link AbstractStatementConverter}s to convert any sub-nodes.
 *
 * @author Lennart Jörelid, jGuru Europe AB
 */
public abstract class AbstractGraphConverter<From> implements TypeConverter<From, Graph> {

    // Our log
    private static final Logger log = LoggerFactory.getLogger(AbstractGraphConverter.class);

    /**
     * Comparator for a sorted map using Class objects for Keys.
     */
    public static final Comparator<Class<?>> CLASS_COMPARATOR = (l, r) -> {

        final String leftClassName = l == null ? "" : l.getName();
        final String rightClassName = r == null ? "" : r.getName();

        // All Done.
        return leftClassName.compareTo(rightClassName);
    };

    /**
     * Comparator for a sorted map using Package objects for Keys.
     */
    public static final Comparator<Package> PACKAGE_COMPARATOR = (l, r) -> {

        final String leftClassName = l == null ? "" : l.getName();
        final String rightClassName = r == null ? "" : r.getName();

        // All Done.
        return leftClassName.compareTo(rightClassName);
    };

    /**
     * The map relating Concept classes - typically something like RootDoc/ClassDoc/PackageDoc from JavaDoc executions,
     * or Class/Package/Method from the java reflection structure. The important thing is that a single
     * {@link AbstractStatementConverter} should be retrieved for each leaf Concept class.
     */
    protected SortedMap<Class<?>, AbstractStatementConverter> statementConverters;

    /**
     * Default constructor.
     */
    protected AbstractGraphConverter() {

        // Create internal state
        this.statementConverters = new TreeMap<>(CLASS_COMPARATOR);
    }

    /**
     * Retrieves an {@link AbstractStatementConverter} subclass which can convert the given Concept
     * class into a {@link Statement}. Null arguments are not permitted.
     *
     * @param conceptClass The class to convert into a Statement.
     * @param <Concept>    The type to convert.
     * @param <S>          The Statement subclass.
     * @return The {@link AbstractStatementConverter} subclass which manages the conversion - or null should none exist.
     */
    public <Concept, S extends Statement> AbstractStatementConverter<Concept, S> get(
            final Class<Concept> conceptClass) {

        // Check sanity
        if (conceptClass == null) {
            throw new NullPointerException("Cannot handle null 'conceptClass' argument.");
        }

        // Fetch stashed converters
        final AbstractStatementConverter<Concept, S> toReturn = statementConverters.get(conceptClass);
        if (toReturn == null && log.isDebugEnabled()) {
            log.debug("No AbstractStatementConverter to convert [" + conceptClass.getName()
                    + "] into a Statement subtype was registered.");
        }

        // All Done.
        return toReturn;
    }

    /**
     * Adds the supplied {@link AbstractStatementConverter} for the given Concept class.
     * Null arguments are not permitted.
     *
     * @param converter    The Converter to add.
     * @param conceptClass The class to convert into a Statement.
     * @param <Concept>    The type to convert.
     * @param <S>          The Statement subclass.
     */
    public <Concept, S extends Statement> void put(final Class<Concept> conceptClass,
                                                   final AbstractStatementConverter<Concept, S> converter) {

        // Check sanity
        if (conceptClass == null) {
            throw new NullPointerException("Cannot handle null 'conceptClass' argument.");
        }
        if (converter == null) {
            throw new NullPointerException("Cannot handle null 'converter' argument.");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Graph convert(final From toConvert) {

        // Handle nulls
        if (toConvert == null && !acceptsNullValues()) {
            throw new IllegalArgumentException("Converter ["
                    + getClass().getName() + "] cannot accept null 'toConvert' arguments.");
        }

        // All Done
        return doConvert(toConvert);
    }

    /**
     * Override this method to convert the supplied non-null toConvert object into a {@link Graph}.
     *
     * @param toConvert The non-null object to convert.
     * @return The converted result.
     */
    protected abstract Graph doConvert(final From toConvert);
}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-checkstyle-maven-plugin
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.plugin.checkstyle.exec;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
import com.puppycrawl.tools.checkstyle.PropertiesExpander;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.FilterSet;
import com.puppycrawl.tools.checkstyle.filters.SuppressionsLoader;
import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.model.Resource;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.resource.ResourceManager;
import org.codehaus.plexus.resource.loader.FileResourceCreationException;
import org.codehaus.plexus.resource.loader.FileResourceLoader;
import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * @author Olivier Lamy
 * @version $Id$
 * @since 2.5
 */
@Component(role = CheckstyleExecutor.class,
        hint = "default",
        instantiationStrategy = "per-lookup")
public class DefaultCheckstyleExecutor extends AbstractLogEnabled implements CheckstyleExecutor {

    @Requirement(hint = "default")
    private ResourceManager locator;

    @Requirement(hint = "license")
    private ResourceManager licenseLocator;

    public CheckstyleResults executeCheckstyle(CheckstyleExecutorRequest request)
            throws CheckstyleExecutorException, CheckstyleException {

        // Checkstyle will always use the context classloader in order
        // to load resources (dtds), so we have to fix it
        // olamy this hack is not anymore needed in Maven 3.x
        ClassLoader checkstyleClassLoader = PackageNamesLoader.class.getClassLoader();
        Thread.currentThread().setContextClassLoader(checkstyleClassLoader);

        if (getLogger().isDebugEnabled()) {
            getLogger().debug("executeCheckstyle start headerLocation : " + request.getHeaderLocation());
        }

        MavenProject project = request.getProject();

        configureResourceLocator(locator, request, null);

        configureResourceLocator(licenseLocator, request, request.getLicenseArtifacts());

        // Config is less critical than License, locator can still be used.
        // configureResourceLocator( configurationLocator, request, request.getArtifactsHoldingCheckstyleConfiguration() );

        List<File> files;
        try {
            files = getFilesToProcess(request);
        } catch (IOException e) {
            throw new CheckstyleExecutorException("Error getting files to process", e);
        }

        final String suppressionsFilePath = getSuppressionsFilePath(request);
        FilterSet filterSet = getSuppressionsFilterSet(suppressionsFilePath);

        Checker checker = new Checker();

        // setup classloader, needed to avoid "Unable to get class information for ..." errors
        List<String> classPathStrings = new ArrayList<>();
        List<String> outputDirectories = new ArrayList<>();

        // stand-alone
        Collection<File> sourceDirectories = null;
        Collection<File> testSourceDirectories = request.getTestSourceDirectories();

        // aggregator
        Map<MavenProject, Collection<File>> sourceDirectoriesByProject = new HashMap<>();
        Map<MavenProject, Collection<File>> testSourceDirectoriesByProject = new HashMap<>();

        if (request.isAggregate()) {
            for (MavenProject childProject : request.getReactorProjects()) {
                sourceDirectories = new ArrayList<>(childProject.getCompileSourceRoots().size());
                List<String> compileSourceRoots = childProject.getCompileSourceRoots();
                for (String compileSourceRoot : compileSourceRoots) {
                    sourceDirectories.add(new File(compileSourceRoot));
                }
                sourceDirectoriesByProject.put(childProject, sourceDirectories);

                testSourceDirectories = new ArrayList<>(childProject.getTestCompileSourceRoots().size());
                List<String> testCompileSourceRoots = childProject.getTestCompileSourceRoots();
                for (String testCompileSourceRoot : testCompileSourceRoots) {
                    testSourceDirectories.add(new File(testCompileSourceRoot));
                }
                testSourceDirectoriesByProject.put(childProject, testSourceDirectories);

                prepareCheckstylePaths(request, childProject, classPathStrings, outputDirectories,
                        sourceDirectories, testSourceDirectories);
            }
        } else {
            sourceDirectories = request.getSourceDirectories();
            prepareCheckstylePaths(request, project, classPathStrings, outputDirectories, sourceDirectories,
                    testSourceDirectories);
        }

        final List<URL> urls = new ArrayList<>(classPathStrings.size());

        for (String path : classPathStrings) {
            try {
                urls.add(new File(path).toURL());
            } catch (MalformedURLException e) {
                throw new CheckstyleExecutorException(e.getMessage(), e);
            }
        }

        for (String outputDirectoryString : outputDirectories) {
            try {
                if (outputDirectoryString != null) {
                    File outputDirectoryFile = new File(outputDirectoryString);
                    if (outputDirectoryFile.exists()) {
                        URL outputDirectoryUrl = outputDirectoryFile.toURL();
                        getLogger().debug("Adding the outputDirectory " + outputDirectoryUrl.toString()
                                + " to the Checkstyle class path");
                        urls.add(outputDirectoryUrl);
                    }
                }
            } catch (MalformedURLException e) {
                throw new CheckstyleExecutorException(e.getMessage(), e);
            }
        }

        URLClassLoader projectClassLoader = AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {
            public URLClassLoader run() {
                return new URLClassLoader(urls.toArray(new URL[urls.size()]), null);
            }
        });

        checker.setClassLoader(projectClassLoader);

        checker.setModuleClassLoader(Thread.currentThread().getContextClassLoader());

        if (filterSet != null) {
            checker.addFilter(filterSet);
        }
        Configuration configuration = getConfiguration(request);
        checker.configure(configuration);

        AuditListener listener = request.getListener();

        if (listener != null) {
            checker.addListener(listener);
        }

        if (request.isConsoleOutput()) {
            checker.addListener(request.getConsoleListener());
        }

        CheckstyleCheckerListener checkerListener = new CheckstyleCheckerListener(configuration);
        if (request.isAggregate()) {
            for (MavenProject childProject : request.getReactorProjects()) {
                sourceDirectories = sourceDirectoriesByProject.get(childProject);
                testSourceDirectories = testSourceDirectoriesByProject.get(childProject);
                addSourceDirectory(checkerListener, sourceDirectories,
                        testSourceDirectories,
                        childProject.getResources(), request);
            }
        } else {
            addSourceDirectory(checkerListener, sourceDirectories, testSourceDirectories, request.getResources(),
                    request);
        }

        checker.addListener(checkerListener);

        int nbErrors = checker.process(files);

        checker.destroy();

        if (projectClassLoader instanceof Closeable) {
            try {
                ((Closeable) projectClassLoader).close();
            } catch (IOException ex) {
                // Nothing we can do - and not detrimental to the build (save running out of file handles).
                getLogger().info("Failed to close custom Classloader - this indicated a bug in the code.", ex);
            }
        }

        if (request.getStringOutputStream() != null) {
            String message = request.getStringOutputStream().toString().trim();

            if (message.length() > 0) {
                getLogger().info(message);
            }
        }

        if (nbErrors > 0) {
            StringBuilder message = new StringBuilder("There ");
            if (nbErrors == 1) {
                message.append("is");
            } else {
                message.append("are");
            }
            message.append(" ");
            message.append(nbErrors);
            message.append(" error");
            if (nbErrors != 1) {
                message.append("s");
            }
            message.append(" reported by Checkstyle");
            String version = getCheckstyleVersion();
            if (version != null) {
                message.append(" ");
                message.append(version);
            }
            message.append(" with ");
            message.append(request.getConfigLocation());
            message.append(" ruleset.");

            if (request.isFailsOnError()) {
                // TODO: should be a failure, not an error. Report is not meant to
                // throw an exception here (so site would
                // work regardless of config), but should record this information
                throw new CheckstyleExecutorException(message.toString());
            } else {
                getLogger().info(message.toString());
            }
        }

        return checkerListener.getResults();
    }

    protected void addSourceDirectory(CheckstyleCheckerListener sinkListener, Collection<File> sourceDirectories,
                                      Collection<File> testSourceDirectories, List<Resource> resources,
                                      CheckstyleExecutorRequest request) {
        if (sourceDirectories != null) {
            for (File sourceDirectory : sourceDirectories) {
                if (sourceDirectory.exists()) {
                    sinkListener.addSourceDirectory(sourceDirectory);
                }
            }
        }

        if (request.isIncludeTestSourceDirectory() && (testSourceDirectories != null)) {
            for (File testSourceDirectory : testSourceDirectories) {
                if (testSourceDirectory.isDirectory()) {
                    sinkListener.addSourceDirectory(testSourceDirectory);
                }
            }
        }

        if (resources != null) {
            for (Resource resource : resources) {
                if (resource.getDirectory() != null) {
                    File resourcesDirectory = new File(resource.getDirectory());
                    if (resourcesDirectory.exists() && resourcesDirectory.isDirectory()) {
                        sinkListener.addSourceDirectory(resourcesDirectory);
                        getLogger().debug("Added '" + resourcesDirectory.getAbsolutePath()
                                + "' as a source directory.");
                    }
                }
            }
        }
    }

    public Configuration getConfiguration(CheckstyleExecutorRequest request)
            throws CheckstyleExecutorException {
        try {
            // Checkstyle will always use the context classloader in order
            // to load resources (dtds),
            // so we have to fix it
            ClassLoader checkstyleClassLoader = PackageNamesLoader.class.getClassLoader();
            Thread.currentThread().setContextClassLoader(checkstyleClassLoader);
            String configFile = getConfigFile(request);
            Properties overridingProperties = getOverridingProperties(request);
            Configuration config = ConfigurationLoader
                    .loadConfiguration(configFile, new PropertiesExpander(overridingProperties));
            String effectiveEncoding = StringUtils.isNotEmpty(request.getEncoding()) ? request.getEncoding() : System
                    .getProperty("file.encoding", "UTF-8");

            if (StringUtils.isEmpty(request.getEncoding())) {
                getLogger().warn("File encoding has not been set, using platform encoding " + effectiveEncoding
                        + ", i.e. build is platform dependent!");
            }

            if ("Checker".equals(config.getName())
                    || "com.puppycrawl.tools.checkstyle.Checker".equals(config.getName())) {
                if (config instanceof DefaultConfiguration) {
                    // MCHECKSTYLE-173 Only add the "charset" attribute if it has not been set
                    try {
                        if (config.getAttribute("charset") == null) {
                            ((DefaultConfiguration) config).addAttribute("charset", effectiveEncoding);
                        }
                    } catch (CheckstyleException ex) {
                        // Checkstyle 5.4+ throws an exception when trying to access an attribute that doesn't exist
                        ((DefaultConfiguration) config).addAttribute("charset", effectiveEncoding);
                    }
                } else {
                    getLogger().warn("Failed to configure file encoding on module " + config);
                }
            }
            Configuration[] modules = config.getChildren();
            for (Configuration module : modules) {
                if ("TreeWalker".equals(module.getName())
                        || "com.puppycrawl.tools.checkstyle.TreeWalker".equals(module.getName())) {
                    if (module instanceof DefaultConfiguration) {
                        // MCHECKSTYLE-132 DefaultConfiguration addAttribute has changed in checkstyle 5.3
                        try {
                            if (module.getAttribute("cacheFile") == null) {
                                ((DefaultConfiguration) module).addAttribute("cacheFile", request.getCacheFile());
                            }
                        } catch (CheckstyleException ex) {
                            // MCHECKSTYLE-159 - checkstyle 5.4 throws an exception instead of return null if
                            // "cacheFile"
                            // doesn't exist
                            ((DefaultConfiguration) module).addAttribute("cacheFile", request.getCacheFile());
                        }
                    } else {
                        getLogger().warn("Failed to configure cache file on module " + module);
                    }
                }
            }
            return config;
        } catch (CheckstyleException e) {
            throw new CheckstyleExecutorException("Failed during checkstyle configuration", e);
        }
    }

    private void prepareCheckstylePaths(CheckstyleExecutorRequest request, MavenProject project,
                                        List<String> classPathStrings, List<String> outputDirectories,
                                        Collection<File> sourceDirectories, Collection<File> testSourceDirectories)
            throws CheckstyleExecutorException {
        try {
            outputDirectories.add(project.getBuild().getOutputDirectory());

            if (request.isIncludeTestSourceDirectory() && (testSourceDirectories != null)
                    && anyDirectoryExists(testSourceDirectories)) {
                classPathStrings.addAll(project.getTestClasspathElements());
                outputDirectories.add(project.getBuild().getTestOutputDirectory());
            } else {
                classPathStrings.addAll(project.getCompileClasspathElements());
            }
        } catch (DependencyResolutionRequiredException e) {
            throw new CheckstyleExecutorException(e.getMessage(), e);
        }
    }

    private boolean anyDirectoryExists(Collection<File> files) {
        for (File file : files) {
            if (file.isDirectory()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get the effective Checkstyle version at runtime.
     *
     * @return the MANIFEST implementation version of Checkstyle API package (can be null)
     * @todo Copied from CheckstyleReportGenerator - move to a utility class
     */
    private String getCheckstyleVersion() {
        Package checkstyleApiPackage = Configuration.class.getPackage();

        return (checkstyleApiPackage == null) ? null : checkstyleApiPackage.getImplementationVersion();
    }

    private Properties getOverridingProperties(CheckstyleExecutorRequest request)
            throws CheckstyleExecutorException {
        Properties p = new Properties();
        InputStream in = null;
        try {
            if (request.getPropertiesLocation() != null) {
                if (getLogger().isDebugEnabled()) {
                    getLogger().debug("request.getPropertiesLocation() " + request.getPropertiesLocation());
                }

                File propertiesFile = locator.getResourceAsFile(request.getPropertiesLocation(),
                        "checkstyle-checker.properties");

                if (propertiesFile != null) {
                    in = new FileInputStream(propertiesFile);
                    p.load(in);
                    in.close();
                    in = null;
                }
            }

            if (StringUtils.isNotEmpty(request.getPropertyExpansion())) {
                String propertyExpansion = request.getPropertyExpansion();
                // Convert \ to \\, so that p.load will convert it back properly
                propertyExpansion = StringUtils.replace(propertyExpansion, "\\", "\\\\");
                p.load(new ByteArrayInputStream(propertyExpansion.getBytes()));
            }

            // Workaround for MCHECKSTYLE-48
            // Make sure that "config/maven-header.txt" is the default value
            // for headerLocation, if configLocation="config/maven_checks.xml"
            String headerLocation = request.getHeaderLocation();
            if ("config/maven_checks.xml".equals(request.getConfigLocation())) {

                if ("LICENSE.txt".equals(request.getHeaderLocation())) {
                    headerLocation = "config/maven-header.txt";
                }
            }
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("headerLocation " + headerLocation);
            }

            if (StringUtils.isNotEmpty(headerLocation)) {
                try {
                    File headerFile = licenseLocator.getResourceAsFile(headerLocation, "checkstyle-header.txt");

                    if (headerFile != null) {
                        p.setProperty("checkstyle.header.file", headerFile.getAbsolutePath());
                    }
                } catch (FileResourceCreationException | ResourceNotFoundException e) {
                    getLogger().debug("Unable to process header location: " + headerLocation);
                    getLogger().debug("Checkstyle will throw exception if ${checkstyle.header.file} is used");
                }
            }

            if (request.getCacheFile() != null) {
                p.setProperty("checkstyle.cache.file", request.getCacheFile());
            }
        } catch (IOException | ResourceNotFoundException | FileResourceCreationException e) {
            throw new CheckstyleExecutorException("Failed to get overriding properties", e);
        } finally {
            IOUtils.closeQuietly(in);
        }
        if (request.getSuppressionsFileExpression() != null) {
            String suppressionsFilePath = getSuppressionsFilePath(request);

            if (suppressionsFilePath != null) {
                p.setProperty(request.getSuppressionsFileExpression(), suppressionsFilePath);
            }
        }

        return p;
    }

    private List<File> getFilesToProcess(CheckstyleExecutorRequest request)
            throws IOException {
        StringBuilder excludesStr = new StringBuilder();

        if (StringUtils.isNotEmpty(request.getExcludes())) {
            excludesStr.append(request.getExcludes());
        }

        String[] defaultExcludes = FileUtils.getDefaultExcludes();
        for (String defaultExclude : defaultExcludes) {
            if (excludesStr.length() > 0) {
                excludesStr.append(",");
            }

            excludesStr.append(defaultExclude);
        }

        Set<File> files = new LinkedHashSet<>();
        if (request.isAggregate()) {
            for (MavenProject project : request.getReactorProjects()) {
                Set<File> sourceDirectories = new LinkedHashSet<>();

                // CompileSourceRoots are absolute paths
                List<String> compileSourceRoots = project.getCompileSourceRoots();
                for (String compileSourceRoot : compileSourceRoots) {
                    sourceDirectories.add(new File(compileSourceRoot));
                }

                Set<File> testSourceDirectories = new LinkedHashSet<>();
                // CompileSourceRoots are absolute paths
                List<String> testCompileSourceRoots = project.getTestCompileSourceRoots();
                for (String testCompileSourceRoot : testCompileSourceRoots) {
                    testSourceDirectories.add(new File(testCompileSourceRoot));
                }

                addFilesToProcess(request, sourceDirectories, project.getResources(), project.getTestResources(),
                        files, testSourceDirectories);
            }
        } else {
            Collection<File> sourceDirectories = request.getSourceDirectories();
            addFilesToProcess(request, sourceDirectories, request.getResources(),
                    request.getTestResources(), files, request.getTestSourceDirectories());
        }

        getLogger().debug("Added " + files.size() + " files to process.");

        return new ArrayList<>(files);
    }

    private void addFilesToProcess(CheckstyleExecutorRequest request, Collection<File> sourceDirectories,
                                   List<Resource> resources, List<Resource> testResources, Collection<File> files,
                                   Collection<File> testSourceDirectories)
            throws IOException {
        if (sourceDirectories != null) {
            for (File sourceDirectory : sourceDirectories) {
                if (sourceDirectory.isDirectory()) {
                    final List<File> sourceFiles =
                            FileUtils.getFiles(sourceDirectory, request.getIncludes(), request.getExcludes());
                    files.addAll(sourceFiles);
                    getLogger().debug("Added " + sourceFiles.size() + " source files found in '"
                            + sourceDirectory.getAbsolutePath() + "'.");
                }
            }
        }

        if (request.isIncludeTestSourceDirectory() && testSourceDirectories != null) {
            for (File testSourceDirectory : testSourceDirectories) {
                if (testSourceDirectory.isDirectory()) {
                    final List<File> testSourceFiles =
                            FileUtils.getFiles(testSourceDirectory, request.getIncludes(), request.getExcludes());

                    files.addAll(testSourceFiles);
                    getLogger().debug("Added " + testSourceFiles.size() + " test source files found in '"
                            + testSourceDirectory.getAbsolutePath() + "'.");
                }
            }
        }

        if (resources != null && request.isIncludeResources()) {
            addResourceFilesToProcess(request, resources, files);
        } else {
            getLogger().debug("No resources found in this project.");
        }

        if (testResources != null && request.isIncludeTestResources()) {
            addResourceFilesToProcess(request, testResources, files);
        } else {
            getLogger().debug("No test resources found in this project.");
        }
    }

    private void addResourceFilesToProcess(CheckstyleExecutorRequest request, List<Resource> resources,
                                           Collection<File> files)
            throws IOException {
        for (Resource resource : resources) {
            if (resource.getDirectory() != null) {
                File resourcesDirectory = new File(resource.getDirectory());
                if (resourcesDirectory.isDirectory()) {
                    String includes = request.getResourceIncludes();
                    String excludes = request.getResourceExcludes();

                    // MCHECKSTYLE-214: Only with project-root respect in/excludes, otherwise you'll get every file
                    if (resourcesDirectory.equals(request.getProject().getBasedir())) {
                        String resourceIncludes = StringUtils.join(resource.getIncludes().iterator(), ",");
                        if (StringUtils.isEmpty(includes)) {
                            includes = resourceIncludes;
                        } else {
                            includes += "," + resourceIncludes;
                        }

                        String resourceExcludes = StringUtils.join(resource.getExcludes().iterator(), ",");
                        if (StringUtils.isEmpty(excludes)) {
                            excludes = resourceExcludes;
                        } else {
                            excludes += "," + resourceExcludes;
                        }
                    }

                    List<File> resourceFiles =
                            FileUtils.getFiles(resourcesDirectory, includes, excludes);
                    files.addAll(resourceFiles);
                    getLogger().debug("Added " + resourceFiles.size() + " resource files found in '"
                            + resourcesDirectory.getAbsolutePath() + "'.");
                } else {
                    getLogger().debug("The resources directory '" + resourcesDirectory.getAbsolutePath()
                            + "' does not exist or is not a directory.");
                }
            }
        }
    }

    private FilterSet getSuppressionsFilterSet(final String suppressionsFilePath)
            throws CheckstyleExecutorException {
        if (suppressionsFilePath == null) {
            return null;
        }

        try {
            return SuppressionsLoader.loadSuppressions(suppressionsFilePath);
        } catch (CheckstyleException ce) {
            throw new CheckstyleExecutorException("Failed to load suppressions file from: "
                    + suppressionsFilePath, ce);
        }
    }

    private String getSuppressionsFilePath(final CheckstyleExecutorRequest request)
            throws CheckstyleExecutorException {
        final String suppressionsLocation = request.getSuppressionsLocation();
        if (StringUtils.isEmpty(suppressionsLocation)) {
            return null;
        }

        try {
            File suppressionsFile = locator.getResourceAsFile(suppressionsLocation, "checkstyle-suppressions.xml");
            return suppressionsFile == null ? null : suppressionsFile.getAbsolutePath();
        } catch (ResourceNotFoundException e) {
            throw new CheckstyleExecutorException("Unable to find suppressions file at location: "
                    + suppressionsLocation, e);
        } catch (FileResourceCreationException e) {
            throw new CheckstyleExecutorException("Unable to process suppressions file location: "
                    + suppressionsLocation, e);
        }
    }

    private String getConfigFile(CheckstyleExecutorRequest request)
            throws CheckstyleExecutorException {
        try {
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("request.getConfigLocation() " + request.getConfigLocation());
            }

            File configFile = locator.getResourceAsFile(request.getConfigLocation(), "checkstyle-checker.xml");
            if (configFile == null) {
                throw new CheckstyleExecutorException("Unable to process config location: "
                        + request.getConfigLocation());
            }
            return configFile.getAbsolutePath();
        } catch (ResourceNotFoundException e) {
            throw new CheckstyleExecutorException("Unable to find configuration file at location: "
                    + request.getConfigLocation(), e);
        } catch (FileResourceCreationException e) {
            throw new CheckstyleExecutorException("Unable to process configuration file at location: "
                    + request.getConfigLocation(), e);
        }

    }

    /**
     * Configures search paths in the resource locator.
     * This method should only be called once per execution.
     *
     * @param request executor request data.
     */
    private void configureResourceLocator(final ResourceManager resourceManager,
                                          final CheckstyleExecutorRequest request,
                                          final List<Artifact> additionalArtifacts) {
        final MavenProject project = request.getProject();
        resourceManager.setOutputDirectory(new File(project.getBuild().getDirectory()));

        // Recurse up the parent hierarchy and add project directories to the search roots
        MavenProject parent = project;
        while (parent != null && parent.getFile() != null) {
            // MCHECKSTYLE-131 ( olamy ) I don't like this hack.
            // (dkulp) Me either.   It really pollutes the location stuff
            // by allowing searches of stuff outside the current module.
            File dir = parent.getFile().getParentFile();
            resourceManager.addSearchPath(FileResourceLoader.ID, dir.getAbsolutePath());
            parent = parent.getParent();
        }
        resourceManager.addSearchPath("url", "");

        // MCHECKSTYLE-225: load licenses from additional artifacts, not from classpath
        if (additionalArtifacts != null) {
            for (Artifact licenseArtifact : additionalArtifacts) {
                try {
                    // MCHECKSTYLE-287, MCHECKSTYLE-294: Ignore null licenseArtifacts ...
                    if (licenseArtifact != null) {
                        if (getLogger().isDebugEnabled()) {
                            getLogger().debug("Adding licenceArtifact [" + licenseArtifact.getGroupId() + ":"
                                    + licenseArtifact.getArtifactId() + ":" + licenseArtifact.getVersion()
                                    + "] to resourceManager.");
                        }
                        resourceManager.addSearchPath("jar", "jar:" + licenseArtifact.getFile().toURI().toURL());
                    }
                } catch (MalformedURLException e) {
                    // noop
                }
            }
        }
    }
}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-visualization-spi-doclet
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.visualization.spi.doclet;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import com.sun.tools.doclets.formats.html.HtmlDoclet;
import com.sun.tools.doclets.internal.toolkit.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jguru.nazgul.tools.visualization.spi.doclet.graphviz.DotFacade;
import se.jguru.nazgul.tools.visualization.spi.doclet.javadoc.JavaDocOption;
import se.jguru.nazgul.tools.visualization.spi.doclet.javadoc.VisualizationWrappedRootDoc;

/**
 * Doclet implementation which synthesizes class diagrams from the given RootDoc object graph.
 *
 * @author Lennart Jörelid, jGuru Europe AB
 */
public class VisualizationDoclet implements SimpleDoclet {

    // Our log
    private static final Logger log = LoggerFactory.getLogger(VisualizationDoclet.class);

    // Internal state
    private HtmlDoclet htmlDoclet;
    private DotFacade dotFacade;

    /**
     * Default constructor.
     */
    public VisualizationDoclet() {
        this(new DotFacade());
    }

    /**
     * Test-friendly state injection constructor.
     *
     * @param dotFacade The DotFacade instance to use
     */
    public VisualizationDoclet(final DotFacade dotFacade) {

        // Check sanity
        if (dotFacade == null) {
            throw new NullPointerException("Cannot handle null 'dotFacade' argument.");
        }

        // Assign internal state
        this.dotFacade = dotFacade;
        this.htmlDoclet = new HtmlDoclet();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean start(final RootDoc root) {

        // Check sanity
        if (this.dotFacade == null) {
            throw new IllegalStateException("The DotFacade cannot be null.");
        }

        // Wrap the received RootDoc instance.
        final VisualizationWrappedRootDoc rootDoc = new VisualizationWrappedRootDoc(root);
        htmlDoclet.configuration.root = root;

        try {
            htmlDoclet.configuration.setOptions();
        } catch (Configuration.Fault fault) {
            fault.printStackTrace();
        }

        for (ClassDoc cDoc : rootDoc.classes()) {

            // Log somewhat
            if (log.isDebugEnabled()) {
                log.debug(getJavaDocAndTagsLog(cDoc));
            }
        }

        for (PackageDoc pDoc : rootDoc.specifiedPackages()) {

            // Log somewhat
            if (log.isDebugEnabled()) {
                log.debug(getJavaDocAndTagsLog(pDoc));
            }
        }

        // Convert the DOT-generated Map and write the image.
        // dotFacade.writePngImageAndImageMap(rootDoc, theDotDiagram, outputDirectory, fileName);

        // Delegate further execution to the standard Doclet.
        return HtmlDoclet.start(root);
    }

    private <T extends Doc> String getJavaDocAndTagsLog(final T theDoc) {

        // Draft up the initial string.
        final String initialString = "Processing " + (theDoc instanceof PackageDoc ? "PackageDoc" : "ClassDoc")
                + " [" + theDoc.name() + "]\n";
        final StringBuilder builder = new StringBuilder(initialString);

        // Add comments and tags
        builder.append("Comment: ").append(theDoc.commentText());
        builder.append("[" + (theDoc.tags() == null ? "0" : theDoc.tags().length) + "] Tags.");

        if (theDoc.tags() != null && theDoc.tags().length > 0) {
            for (Tag current : theDoc.tags()) {
                builder.append("Tag [" + current.name() + "]: " + current.text() + "\n");
            }
        }

        // All Done.
        return builder.toString();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean validateJavaDocOptions(final String[][] options, final DocErrorReporter errorReporter) {

        // This Visualization Doclet recognizes no special options.
        // Simply delegate to the standard doclet.
        try {
            htmlDoclet.sharedInstanceForOptions.setOptions(options);
        } catch (Configuration.Fault fault) {
            fault.printStackTrace();
        }

        // All done?
        return HtmlDoclet.validOptions(options, errorReporter);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int optionLength(final String option) {

        // #1) Handle the -help option.
        if (JavaDocOption.HELP.getOption().equals(option)) {

            // First, provide the help text from the Standard Doclet.
            final int toReturn = HtmlDoclet.optionLength(option);

            // Print the options provided by VisualizationDoclet.
            System.out.println();
            System.out.println("Provided by " + VisualizationDoclet.class.getName() + ":");
            for (JavaDocOption current : JavaDocOption.values()) {
                if (current != JavaDocOption.HELP) {
                    System.out.println(current.getOption() + " " + current.getHelpText());
                }
            }

            // Delegate to the standard Doclet implementation.
            return toReturn;
        }

        // #2) Handle all JavaDoc options known to this Doclet
        for (JavaDocOption current : JavaDocOption.values()) {
            if (current.getOption().equals(option)) {
                return current.getOptionLength();
            }
        }

        // #3) Delegate to the standard Doclet.
        return HtmlDoclet.optionLength(option);
    }
}


package nl.pwiddershoven.jfactory;

import nl.pwiddershoven.jfactory.annotations.AfterFactoryBuild;
import nl.pwiddershoven.jfactory.types.LazyValue;
import nl.pwiddershoven.jfactory.types.Sequence;
import nl.pwiddershoven.jfactory.types.Trait;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;

public abstract class ObjectFactory<T> {

    private Class<T> factoryClass;
    private Map<String, Trait> traits = new HashMap<String, Trait>();
    private Map<String, String[]> innerFactories = new HashMap<String, String[]>();
    private Map<String, Object> propertyValues = new HashMap<String, Object>();
    private Map<String, Object> fieldValues = new HashMap<String, Object>();
    private Object[] constructorArgs;

    // map of sequences by name for factory classes
    private static Map<Class, Map<String, Integer>> sequences = new HashMap<Class, Map<String, Integer>>();

    /** Public **/

    public ObjectFactory(Class<T> factoryClass) {
        this.factoryClass = factoryClass;

        define(); //define the factory, calls subclasses.
    }

    /**
     * Build object.
     * @return
     */
    public T build(Object... attributes) {
        List<Object> attributeList = new ArrayList<Object>(Arrays.asList(attributes)); //kinda wacky but Arrays.asList returns a unmodifiable list

        String[] traitNames = getTraitNames(attributeList);
        applyTraits(traitNames);

        T object = ReflectionUtils.createObject(factoryClass, constructorArgs);

        // merge default properties with supplied attributes
        Map<String, Object> propertyValues = createObjectPropertyValues(this.propertyValues, attributeList);

        // now set properties and fields to the created object
        setProperties(object, propertyValues);
        setFields(object, fieldValues);

        executeCallbacks(AfterFactoryBuild.class, object);
        return object;
    }

    /** DSL methods **/

    /**
     * Method that defines the factory. Should be implemented by subclasses.
     * The implementation of this method should call dsl methods below, to populate the factory.
     */
    protected abstract void define();

    /**
     * Register a trait for this factory
     * @param trait
     */
    protected void trait(String name, Trait trait) {
        traits.put(name, trait);
    }

    /**
     * Register a static value for the given field, can be private.
     * @param name name of the field
     * @param value value that should be assigned to each built object for the given field
     */
    protected void field(String name, Object value) {
        fieldValues.put(name, value);
    }

    /**
     * Register a sequence value for a given field
     * @param name name of the field
     * @param seq Sequence object, to be evaluated later
     */
    protected void field(String name, Sequence seq) {
        field(name, (Object)seq);
    }

    /**
     * Register a lazy value for a given field
     * @param name name of the field
     * @param value Lazy value, to be evaluated later
     */
    protected void field(String name, LazyValue value) {
        field(name, (Object)value);
    }

    /**
     * Register a static value for the given property
     * @param name name of the property
     * @param value value that should be assigned to each built object for the given property
     */
    protected void property(String name, Object value) {
        propertyValues.put(name, value);
    }

    /**
     * Register a sequence value for a given property
     * @param name name of the property
     * @param seq Sequence object, to be evaluated later
     */
    protected void property(String name, Sequence seq) {
        property(name, (Object)seq);
    }

    /**
     * Register a lazy value for a given property
     * @param name name of the property
     * @param value Lazy value, to be evaluated later
     */
    protected void property(String name, LazyValue value) {
        property(name, (Object)value);
    }

    protected void factory(String name, String[] traits) {
        innerFactories.put(name, traits);
    }

    public static String[] traits(String... traits) {
        return traits;
    }

    protected void constructWith(Object... args) {
        this.constructorArgs = args;
    }

    /** Protected methods **/

    protected Class<T> getFactoryClass() {
        return factoryClass;
    }

    protected void executeCallbacks(Class<? extends Annotation> annotationType, T object) {
        //first gather a list of all methods down the inheritance chain which applied the given annotation
        List<Method> annotatedMethods = new ArrayList<Method>();

        Class clz = getClass();
        while(ObjectFactory.class.isAssignableFrom(clz)) {
            // add the methods to the beginning of the method list, so we get a list ordered by position in the inheritance chain
            annotatedMethods.addAll(0, ReflectionUtils.getAnnotatedMethods(clz, annotationType));
            clz = clz.getSuperclass();
        }

        // now call all methods
        for(Method method : annotatedMethods) {
            ReflectionUtils.invokeMethod(this, method, object);
        }
    }

    /** Private methods **/

    private void setProperty(Object target, String name, Object value) {
        if(! ReflectionUtils.setProperty(target, name, getValue(value))) {
            // no property was found, try to set the field directly
            setField(target, name, value);
        }
    }

    private void setField(Object target, String name, Object value) {
        ReflectionUtils.setField(target, name, getValue(value));
    }

    private void setProperties(T object, Map<String, Object> propertyValues) {
        for(String property: propertyValues.keySet()) {
            Object value = propertyValues.get(property);
            if(value instanceof Sequence) {
                value = ((Sequence) value).apply(currentSequence(property)); //lazy evaluate sequence. TODO: see how to make this nicer/more generic
            }

            setProperty(object, property, value);
        }
    }

    private void setFields(T object, Map<String, Object> fieldValues) {
        for(String field : fieldValues.keySet()) {
            Object value = fieldValues.get(field);
            setField(object, field, value);
        }
    }

    private Object getValue(Object value) {
        return value instanceof LazyValue ? ((LazyValue) value).evaluate() : value;
    }

    /**
     * Merge passed attributes with the supplied property values.
     * @param defaultPropertyValues
     * @param attributes
     * @return
     */
    private Map<String, Object> createObjectPropertyValues(Map<String, Object> defaultPropertyValues, List<Object> attributes) {
        Map<String, Object> propertyValues = new HashMap<String, Object>(defaultPropertyValues);

        if(attributes != null) {
            Iterator<Object> iterator = attributes.iterator();
            Map<String, Object> propertyOverrideMap = new HashMap<String, Object>();

            while(iterator.hasNext()) {
                String name = (String)iterator.next();

                // we can only create a map entry if we have both a key and value, so make sure there's a value left
                if(iterator.hasNext()) {
                    Object object = iterator.next();
                    propertyOverrideMap.put(name, object);
                }
            }

            propertyValues.putAll(propertyOverrideMap);
        }

        return propertyValues;
    }

    private String[] getTraitNames(List<Object> attributes) {
        String[] traitNames = null;

        // check if the first attribute matches the name of a defined trait.
        // Also check if the number of attributes is odd. If it's even, we'll assume it's a number of key/value pairs
        // for properties and not meant to apply a trait.
        if(attributes.size() > 0 && attributes.size() %2 != 0) {
            String firstAttribute = (String)attributes.get(0);

            if(traits.containsKey(firstAttribute)) {
                // the first attribute matched the name of a defined trait, assume the trait was meant to be applied
                traitNames = new String[] {firstAttribute};
            }else if(innerFactories.containsKey(firstAttribute)) {
                // first attribute matched the name of a defined factory, apply it's traits
                traitNames = innerFactories.get(firstAttribute);
            }

            if(traitNames != null) {
                // first attribute was a valid trait name or factory, remove it from the list of attributes
                attributes.remove(0);
            }
        }

        return traitNames;
    }

    private void applyTraits(String[] traitNames) {
        if(traitNames != null) {
            // traits were defined, apply them
            for(String traitName : traitNames) {
                Trait t = traits.get(traitName);
                t.apply();
            }
        }
    }

    /**
     * Retrieve the current sequence value for the given property.
     * @param name name of the property that should be assigned the sequence value.
     * @return The current value of the referenced sequence.
     */
    private int currentSequence(String name) {
        Map<String, Integer> sequencesForClass = sequences.get(getClass());
        if(sequencesForClass == null) {
            sequencesForClass = new HashMap<String, Integer>();
            sequences.put(getClass(), sequencesForClass);
        }

        Integer seq = sequencesForClass.get(name);
        seq = seq == null ? 1 : seq + 1;

        sequencesForClass.put(name, seq);
        return seq;
    }
}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-visualization-api
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.visualization.api.dot;

import se.jguru.nazgul.tools.visualization.api.AbstractStringRenderer;
import se.jguru.nazgul.tools.visualization.api.RenderConfiguration;
import se.jguru.nazgul.tools.visualization.model.diagram.statement.Statements;
import se.jguru.nazgul.tools.visualization.model.diagram.statement.Subgraph;

/**
 * Subgraph statement Renderer, complying to the specification in the
 * DOT language specification.
 *
 * @author Lennart Jörelid, jGuru Europe AB
 */
public class SubgraphRenderer extends AbstractStringRenderer<Subgraph> {

    /**
     * Default constructor.
     */
    public SubgraphRenderer() {

        // Delegate
        super(Subgraph.class);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String doRender(final RenderConfiguration config, final Subgraph subgraph) {

        final String prologue = config.getIndent() + "subgraph "
                + quote(subgraph.getId())
                + " { "
                + config.getNewline();

        // Render the statements within this subgraph.
        final Statements statements = subgraph.getStatements();
        final StatementsRenderer statementsRenderer = new StatementsRenderer();
        final String renderedStatements = statementsRenderer.render(config.cloneAndChangeIndentation(1), statements);

        // All Done.
        return prologue + renderedStatements + config.getNewline() + config.getIndent() + "}";
    }
}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-visualization-api
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.visualization.api.dot;

import se.jguru.nazgul.tools.visualization.api.AbstractStringRenderer;
import se.jguru.nazgul.tools.visualization.api.RenderConfiguration;
import se.jguru.nazgul.tools.visualization.model.diagram.statement.CommonNodeAttributes;

/**
 * @author Lennart Jörelid, jGuru Europe AB
 */
public class CommonNodeAttributesRenderer extends AbstractStringRenderer<CommonNodeAttributes> {

    // Internal state
    private AttributeRenderer attributeRenderer;

    /**
     * Default constructor.
     */
    public CommonNodeAttributesRenderer() {

        // Delegate
        super(CommonNodeAttributes.class);

        // Assign internal state
        attributeRenderer = new AttributeRenderer();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String doRender(final RenderConfiguration config, final CommonNodeAttributes entity) {
        return config.getIndent()
                + entity.getId()
                + " " + attributeRenderer.render(config, entity.getAttributes());
    }
}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-visualization-spi-doclet
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.visualization.spi.doclet.converter;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.PackageDoc;
import se.jguru.nazgul.tools.visualization.api.factory.AbstractGraphConverter;
import se.jguru.nazgul.tools.visualization.model.diagram.Graph;
import se.jguru.nazgul.tools.visualization.model.diagram.attribute.GraphAttributes;
import se.jguru.nazgul.tools.visualization.model.diagram.statement.CommonGraphAttributes;
import se.jguru.nazgul.tools.visualization.model.diagram.statement.Subgraph;

import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;

/**
 * AbstractGraphConverter instance intended to synthesize a {@link Graph} containing
 * the full detail as required when illustrating the classes only within a package.
 *
 * @author Lennart Jörelid, jGuru Europe AB
 */
public class PackageDocConverter extends AbstractGraphConverter<Set<ClassDoc>> {

    // Internal state
    private Package relevantPackage;

    /**
     * Compound constructor creating a {@link PackageDocConverter} which extracts information
     * only for the supplied {@link Package}.
     *
     * @param relevantPackage The package for which this AbstractGraphConverter should extract
     *                        its data.
     */
    public PackageDocConverter(final Package relevantPackage) {
        this.relevantPackage = relevantPackage;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected Graph doConvert(final Set<ClassDoc> toConvert) {

        final ClassDoc[] clDocs = toConvert.toArray(new ClassDoc[toConvert.size()]);
        final SortedMap<PackageDoc, SortedSet<ClassDoc>> clPerPackage = Comparators.sortClassesPerPackage(clDocs);

        final Map.Entry<PackageDoc, SortedSet<ClassDoc>> packageAndClasses = clPerPackage
                .entrySet()
                .stream()
                .filter(e -> relevantPackage.getName().equalsIgnoreCase(e.getKey().name()))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("No retrieved classes in expected package ["
                        + relevantPackage.getName() + "]. Found: "
                        + clPerPackage.keySet().stream().map(Doc::name).reduce((l, r) -> l + ", r")
                        .orElse("<none>")));

        final PackageDoc packageDoc = packageAndClasses.getKey();
        final SortedSet<ClassDoc> classDocs = packageAndClasses.getValue();

        // Create the Graph.
        final Graph toReturn = new Graph("Graph_" + packageDoc.name(), true, true);

        final Subgraph packageSubgraph = new Subgraph(packageDoc.name());
        final CommonGraphAttributes subgraphAttrs = new CommonGraphAttributes();
        packageSubgraph.add(subgraphAttrs);

        final GraphAttributes subAttrs = subgraphAttrs.getAttributes();
        subAttrs.label = "Package: " + packageDoc.name();

        // Add a Subgraph for the package
        toReturn.add(renderPackageSubGraph(classDocs));

        // All Done.
        return toReturn;
    }

    /**
     * Renders a Subgraph with the supplied ClassDocs.
     *
     * @param classDocs The ClassDocs to render.
     * @return The rendered SubGraph.
     */
    public Subgraph renderPackageSubGraph(final SortedSet<ClassDoc> classDocs) {

        // TODO: Continue here.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}