/*
 * 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.regioncomparator;

import java.util.*;
import java.util.concurrent.*;

import com.gemstone.gemfire.admin.AdminDistributedSystem;
import com.gemstone.gemfire.admin.AdminDistributedSystemFactory;
import com.gemstone.gemfire.cache.client.Pool;
import com.gemstone.gemfire.cache.client.PoolManager;
import com.gemstone.gemfire.cache.client.internal.AutoConnectionSourceImpl;
import com.gemstone.gemfire.cache.client.internal.PoolImpl;
import com.gemstone.gemfire.cache.execute.FunctionService;
import com.gemstone.gemfire.cache.execute.ResultCollector;
import com.gemstone.gemfire.distributed.internal.ServerLocation;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.googlecode.icegem.cacheutils.Tool;
import com.googlecode.icegem.cacheutils.common.Utils;

public class CompareTool extends Tool {
    private static final Logger log = LoggerFactory.getLogger(CompareTool.class);
    private static String serversOption = "";
    private static String regionName;
    private static List<String> scanPackagesOption;
    private static Properties locatorsProperties = new Properties();

    protected void parseCommandLineArguments(String[] commandLineArguments) {
        Options options = constructGnuOptions();
        if (commandLineArguments.length < 1) {
            printHelp(options);
            Utils.exitWithSuccess();
        }
        CommandLineParser parser = new GnuParser();
        try {
            CommandLine line = parser.parse(options, commandLineArguments);
            if (!line.hasOption("region") || line.hasOption("help")
                    || !(line.hasOption("locators") || line.hasOption("servers"))) {
                printHelp(options);
                Utils.exitWithSuccess();
            }
            if (line.hasOption("packages"))
                scanPackagesOption = Arrays.asList(line.getOptionValue("packages").split(","));
            regionName = line.getOptionValue("region");
            if (line.hasOption("locators"))
                locatorsProperties = line.getOptionProperties("locators");
            if (line.hasOption("servers"))
                serversOption = line.getOptionValue("servers");

        } catch (ParseException exp) {
            System.err.println("Parsing options failed. " + exp.getMessage());
            printHelp(options);
            Utils.exitWithSuccess();
        }
    }

    protected void printHelp(final Options options) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("compare [options]", options);
    }

    protected Options constructGnuOptions() {
        final Options gnuOptions = new Options();
        Option locators = OptionBuilder.hasArgs()
                .withDescription("Locators of GemFire system. For intra-cluster checking. Example: host1[port1],host2[port2]")
                .withValueSeparator()
                .withArgName("cluster=locators")
                .withLongOpt("locators")
                .create("l");
        gnuOptions
                .addOption("r", "region", true, "Region path to be compared. Only replicated region could be used. Example: /region1/region2")
                .addOption("s", "servers", true, "Servers of GemFire system. For multi-cluster systems. Example: host1[port1],host2[port2]")
                .addOption(locators)
                .addOption("c", "packages", true, "Enumerate packages to scan for @AutoSerializable model classes. Delimiter is a comma sign.")
                .addOption("h", "help", false, "Print usage information");
        return gnuOptions;
    }

    public void execute(String[] args, boolean debugEnabled, boolean quiet) {
        AdminDistributedSystem adminDs = AdminDistributedSystemFactory.getDistributedSystem(
                AdminDistributedSystemFactory.defineDistributedSystem()
        );
        adminDs.connect();

        parseCommandLineArguments(args);

        List<Pool> poolList = new ArrayList<Pool>();
        if (serversOption != null  && serversOption.length() > 0)
            for (String serverOption : serversOption.split(",")) {
                String serverHost = serverOption.substring(0, serverOption.indexOf("["));
                String serverPort = serverOption.substring(serverOption.indexOf("[") + 1, serverOption.indexOf("]"));
                poolList.add(PoolManager.createFactory().addServer(serverHost,Integer.parseInt(serverPort))
                            .create("poolTo" + serverHost + serverPort));
            }
        if (locatorsProperties != null && !locatorsProperties.isEmpty())
            for (Object poolOption : locatorsProperties.keySet()) {
                String locator = (String) locatorsProperties.get(poolOption);
                String serverHost = locator.substring(0, locator.indexOf("["));
                String serverPort = locator.substring(locator.indexOf("[") + 1, locator.indexOf("]"));
                poolList.add(PoolManager.createFactory().addLocator(serverHost, Integer.parseInt(serverPort))          //todo: check when we have two identical locators options: exception a pool name already exist
                        .create("poolTo" + serverHost + serverPort));
            }

        //todo: insert checking that each cluster contains region and one's type is equal (Partitioned, Replicated)

        boolean partitioned = false;        //todo: insert CLI usage  + throw exception if real region has another type

        List<ServerLocation> serverFromPool = new ArrayList<ServerLocation>();
        List<Pool> emptyPools = new ArrayList<Pool>(); //contains pool with no available servers
        for (Pool pool : poolList) {
            List<ServerLocation> allServers = null;
            if (!pool.getLocators().isEmpty())
                allServers = ((AutoConnectionSourceImpl) ((PoolImpl) pool).getConnectionSource()).findAllServers();  //todo: ConnectionError if locator doesn't exist
            else if (!pool.getServers().isEmpty())
                allServers = Arrays.asList((((PoolImpl) pool).getConnectionSource()).findServer(Collections.emptySet()));

            if (allServers != null)
                serverFromPool.addAll(allServers);
            else {
                log.info("not found servers on locator {}", pool);
                emptyPools.add(pool);
            }
        }
        poolList.removeAll(emptyPools);

        if (serverFromPool.size() == 0) {
            log.info("no servers available");
            return;
        }

        printServerLocationDetails(serverFromPool);

        //source for comparison //todo: if this node doesn't contain region! it's problem
        Pool sourcePool;
        if (!partitioned) {
            int randomServerLocation = new Random().nextInt(serverFromPool.size());
            sourcePool = PoolManager.createFactory()
                    .addServer(serverFromPool.get(randomServerLocation).getHostName(),
                            serverFromPool.get(randomServerLocation).getPort()).create("target");
        } else {
            sourcePool = poolList.get(0);
            poolList.remove(0);
        }

        FunctionService.registerFunction(new RegionInfoFunction());
        ResultCollector regionInfoResult = FunctionService.onServers(sourcePool)
                .withArgs(regionName)
                .execute(new RegionInfoFunction());

        Map regionInfo = (HashMap) ((ArrayList) regionInfoResult.getResult()).get(0);
        System.out.println("region info: " + regionInfo);

        int totalNumBuckets = (Integer) regionInfo.get("totalNumBuckets");
        //log.debug("total keys' batch counts is ", totalNumBuckets);
        System.out.println("total keys' batch counts is " + totalNumBuckets);
        KeyExtractor keyExtractor = new KeyExtractor(regionName, sourcePool, partitioned, totalNumBuckets);

        Map<String, Map<String, Set>> clusterDifference = new HashMap<String, Map<String, Set>>();          //key: memeberId list: absent keys, diff values

        List<PoolResult> taskResults = new ArrayList<PoolResult>();
        List<Future<PoolResult>> collectTasks = new ArrayList<Future<PoolResult>>(poolList.size());
        ExecutorService executorService = Executors.newFixedThreadPool(poolList.size());
        while (keyExtractor.hasKeys()) {
            Set keys = keyExtractor.getNextKeysBatch();
            System.out.println("keys to check: " + keys);
            for (Pool nextPool : poolList)
                collectTasks.add(executorService.submit(new CollectorTask(keys, nextPool, regionName)));
            System.out.println("active tasks: " + collectTasks.size());
            try {
                //for (Future<ResultCollector> futureTask : collectTasks) {
                for (Future<PoolResult> futureTask : collectTasks) {
                    taskResults.add(futureTask.get());
                }
            } catch (InterruptedException ie) {
                ie.printStackTrace();
            } catch (ExecutionException ee) {
                ee.printStackTrace();
            }
            collectTasks.clear();

            System.out.println("compare contents..");
            //getting source contents
            Map sourceData = new HashMap();

            //getting source map
            FutureTask<PoolResult> ft = new FutureTask<PoolResult>(new CollectorTask(keys, sourcePool, regionName));
            ft.run();
            try {
                PoolResult rc = ft.get();
                List poolResult = (List) rc.getResultCollector().getResult();
                for (Object singleResult : poolResult) {
                    sourceData.putAll((Map) ((HashMap) singleResult).get("map"));
                }
            } catch (Exception e) {
                throw new RuntimeException("error getting key-hash from pool: " + sourcePool, e);
            }
            //todo: aggregate members' data from one cluster

            System.out.println("source data is: " + sourceData);
            //for (ResultCollector taskResultFromPool : taskResults) {
            for (PoolResult taskResultFromPool : taskResults) {
                List poolResult = (ArrayList) taskResultFromPool.getResultCollector().getResult();
                if (!partitioned) {
                    for (Object resultFromMember : poolResult) {
                        Map result = (HashMap) resultFromMember;
                        String memberId = (String) result.get("memberId");
                        if (regionInfo.get("id").equals(result.get("memberId")))                   //for replicated region
                            continue;
                        Map<String, Set> aggregationInfo = compareAndAggregate(sourceData, (HashMap) result.get("map"));
                        System.out.println("result of comparing is: " + aggregationInfo);
                        if (!clusterDifference.containsKey(memberId)) {
                            aggregationInfo.put("absentKeys", new HashSet());
                            clusterDifference.put(memberId, aggregationInfo);
                        } else {
                            Map<String, Set> difference = clusterDifference.get(memberId);
                            difference.get("absentKeys").addAll((Set) result.get("absentKeys"));
                            difference.get("diffValues").addAll(aggregationInfo.get("diffValues"));
                            clusterDifference.put(memberId, difference);
                        }
                    }
                } else {
                    Map targetData = new HashMap();
                    Set absentKeysFromPool = new HashSet();

                    //aggregate data from different members with partition region
                    for (Object resultFromMember: poolResult) {
                        targetData.putAll((Map) ((HashMap) resultFromMember).get("map"));
                        absentKeysFromPool.addAll((Set) ((HashMap) resultFromMember).get("absentKeys"));
                    }

                    Map<String, Set> aggregationInfo = compareAndAggregate(sourceData, targetData);
                    System.out.println("result of comparing is: " + aggregationInfo);
                    String keyForPartitionRegionType = taskResultFromPool.getPool().toString();
                    if (!clusterDifference.containsKey(keyForPartitionRegionType)) {
                            clusterDifference.put(keyForPartitionRegionType, aggregationInfo);
                        } else {
                            Map<String, Set> difference = clusterDifference.get(keyForPartitionRegionType);
                            difference.get("absentKeys").addAll(aggregationInfo.get("absentKeys"));
                            difference.get("diffValues").addAll(aggregationInfo.get("diffValues"));
                            clusterDifference.put(keyForPartitionRegionType, difference);
                        }
                }
            }

            taskResults.clear();
        }

        System.out.println("____________________________");
        System.out.println("difference: ");
        System.out.println(clusterDifference);
        executorService.shutdown();
        adminDs.disconnect();
    }

    /**
     * compare two snapshots from source and target maps
     * format: key: absentKeys or map
     */
    private static Map<String, Set> compareAndAggregate(Map sourceMap, Map targetMap) {
        //keys witch values are different
        System.out.println("compare maps");
        System.out.println("source: " + sourceMap);
        System.out.println("target: " + targetMap);
        Map<String, Set> aggregationInfo = new HashMap<String, Set>();
        Set<Object> keysForDiffValues = new HashSet<Object>();
        for (Object regionKey : targetMap.keySet()) {
            if (!targetMap.get(regionKey).equals(sourceMap.get(regionKey)))      //we compare not original values, but it's hash code
                keysForDiffValues.add(regionKey);
        }

        aggregationInfo.put("diffValues", keysForDiffValues);
        return aggregationInfo;
    }

    private void printServerLocationDetails(List<ServerLocation> serverLocationList) {
        for (ServerLocation server : serverLocationList) {
            System.out.println("host: " + server.getHostName());
            System.out.println("port: " + server.getPort());
            System.out.println("----------------------");
        }
    }
}


/*
  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;

import static ch.lambdaj.Lambda.filter;
import static ch.lambdaj.Lambda.having;
import static ch.lambdaj.Lambda.on;
import static javax.persistence.GenerationType.IDENTITY;
import static org.hamcrest.Matchers.equalTo;

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.JoinTable;
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.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
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.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.htmlparser.jericho.Source;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;
import org.hibernate.envers.RelationTargetAuditMode;
import org.hibernate.validator.constraints.NotBlank;
import org.jboss.pressgang.ccms.model.base.ParentToPropertyTag;
import org.jboss.pressgang.ccms.model.constants.Constants;
import org.jboss.pressgang.ccms.model.contentspec.CSInfoNode;
import org.jboss.pressgang.ccms.model.contentspec.CSNode;
import org.jboss.pressgang.ccms.model.contentspec.ContentSpec;
import org.jboss.pressgang.ccms.model.exceptions.CustomConstraintViolationException;
import org.jboss.pressgang.ccms.model.interfaces.HasTags;
import org.jboss.pressgang.ccms.model.interfaces.HasTwoWayRelationships;
import org.jboss.pressgang.ccms.model.sort.TagIDComparator;
import org.jboss.pressgang.ccms.model.sort.TopicIDComparator;
import org.jboss.pressgang.ccms.model.sort.TopicToTopicMainTopicIDSort;
import org.jboss.pressgang.ccms.model.sort.TopicToTopicRelatedTopicIDSort;
import org.jboss.pressgang.ccms.model.utils.TopicUtilities;
import org.jboss.pressgang.ccms.utils.common.CollectionUtilities;
import org.jboss.pressgang.ccms.utils.constants.CommonConstants;

@Entity
@Audited
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Table(name = "Topic")
public class Topic extends ParentToPropertyTag<Topic, TopicToPropertyTag> implements HasTags, HasTwoWayRelationships<TopicToTopic>,
        Serializable {
    public static final String SELECT_ALL_QUERY = "SELECT topic FROM Topic as Topic";

    /*
     * The name of the Hibernate Search field
     */
    public static final String TOPIC_SEARCH_TEXT_FIELD_NAME = "TopicSearchText";
    private static final long serialVersionUID = 5580473587657911655L;

    private Integer topicId;
    private String topicText;
    private Date topicTimeStamp;
    private String topicTitle;
    private Integer xmlFormat = CommonConstants.DOCBOOK_45;
    private Set<TopicToTag> topicToTags = new HashSet<TopicToTag>(0);
    private Set<TopicToTopic> parentTopicToTopics = new HashSet<TopicToTopic>(0);
    private Set<TopicToTopic> childTopicToTopics = new HashSet<TopicToTopic>(0);
    private Set<TopicToTopicSourceUrl> topicToTopicSourceUrls = new HashSet<TopicToTopicSourceUrl>(0);
    private Set<TopicToPropertyTag> topicToPropertyTags = new HashSet<TopicToPropertyTag>(0);
    private Set<TopicToBugzillaBug> topicToBugzillaBugs = new HashSet<TopicToBugzillaBug>(0);
    private Set<MinHash> minHashes = new HashSet<MinHash>(0);
    private String topicXML;
    private TopicSecondOrderData topicSecondOrderData;
    private Locale locale;
    private char[] topicContentHash;

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

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "TopicID", unique = true, nullable = false)
    public Integer getTopicId() {
        return topicId;
    }

    public void setTopicId(final Integer topicId) {
        this.topicId = topicId;
    }

    @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "LocaleID")
    @NotNull(message = "{topic.locale.notBlank}")
    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    public Locale getLocale() {
        return locale;
    }

    public void setLocale(final Locale topicLocale) {
        this.locale = topicLocale;
    }

    @Column(name = "TopicXMLFormat", nullable = false)
    @NotNull
    public Integer getXmlFormat() {
        return xmlFormat;
    }

    public void setXmlFormat(Integer xmlDoctype) {
        this.xmlFormat = xmlDoctype;
    }

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

    public void setMinHashes(final Set<MinHash> minHashes) {
        this.minHashes = minHashes;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "relatedTopic")
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<TopicToTopic> getChildTopicToTopics() {
        return childTopicToTopics;
    }

    public void setChildTopicToTopics(final Set<TopicToTopic> childTopicToTopics) {
        this.childTopicToTopics = childTopicToTopics;
    }

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

    public void setParentTopicToTopics(final Set<TopicToTopic> parentTopicToTopics) {
        this.parentTopicToTopics = parentTopicToTopics;
    }

    @OneToOne(fetch = FetchType.LAZY, optional = true, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinTable(name = "TopicToTopicSecondOrderData", joinColumns = { @JoinColumn(name = "TopicID", unique = true) }, inverseJoinColumns = { @JoinColumn(name = "TopicSecondOrderDataID") })
    @NotAudited
    public TopicSecondOrderData getTopicSecondOrderData() {
        return topicSecondOrderData;
    }

    public void setTopicSecondOrderData(TopicSecondOrderData topicSecondOrderData) {
        this.topicSecondOrderData = topicSecondOrderData;
    }

    @Column(name = "TopicText", columnDefinition = "TEXT")
    @Size(max = 65535)
    public String getTopicText() {
        return this.topicText;
    }

    public void setTopicText(final String topicText) {
        this.topicText = topicText;
    }

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "TopicTimeStamp", nullable = false, length = 0)
    @NotNull
    public Date getTopicTimeStamp() {
        return this.topicTimeStamp;
    }

    public void setTopicTimeStamp(final Date topicTimeStamp) {
        this.topicTimeStamp = topicTimeStamp;
    }

    @Column(name = "TopicTitle", nullable = false, length = 1024)
    @NotNull(message = "{topic.title.notBlank}")
    @NotBlank(message = "{topic.title.notBlank}")
    @Size(max = 1024)
    public String getTopicTitle() {
        return this.topicTitle;
    }

    public void setTopicTitle(final String topicTitle) {
        this.topicTitle = topicTitle;
    }

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

    public void setTopicToTags(final Set<TopicToTag> topicToTags) {
        this.topicToTags = topicToTags;
    }

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

    public void setTopicToTopicSourceUrls(final Set<TopicToTopicSourceUrl> topicToTopicSourceUrls) {
        this.topicToTopicSourceUrls = topicToTopicSourceUrls;
    }

    @Column(name = "TopicXML", columnDefinition = "MEDIUMTEXT")
    @Size(max = 16777215)
    public String getTopicXML() {
        return topicXML;
    }

    public void setTopicXML(final String topicXML) {
        this.topicXML = topicXML;
    }

    @Column(name = "TopicContentHash", columnDefinition = "CHAR(64)")
    @Size(max = 64, min = 64)
    public char[] getTopicContentHash() {
        return topicContentHash;
    }

    public void setTopicContentHash(final char[] topicContentHash) {
        this.topicContentHash = topicContentHash;
    }

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

    public void setTopicToPropertyTags(Set<TopicToPropertyTag> topicToPropertyTags) {
        this.topicToPropertyTags = topicToPropertyTags;
    }

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

    public void setTopicToBugzillaBugs(final Set<TopicToBugzillaBug> topicToBugzillaBugs) {
        this.topicToBugzillaBugs = topicToBugzillaBugs;
    }

    @Transient
    public List<MinHash> getMinHashesList() {
        final List<MinHash> retValue = new ArrayList<MinHash>();
        for (final MinHash mapping : minHashes) {
            retValue.add(mapping);
        }

        return retValue;
    }

    /**
     * This function will take the XML in the topicXML String and use it to generate a text only view.
     * The text extraction uses Jericho - http://jericho.htmlparser.net/
     */
    @Transient
    public String getTopicSearchText() {
        if (topicXML == null) return "";

        final Source source = new Source(topicXML);
        source.fullSequentialParse();
        return source.getTextExtractor().toString();
    }

    @Transient
    public String getTopicXMLErrors() {
        if (topicSecondOrderData == null) return null;

        return topicSecondOrderData.getTopicXMLErrors();
    }

    public void setTopicXMLErrors(final String value) {
        if (topicSecondOrderData == null) {
            topicSecondOrderData = new TopicSecondOrderData();
        }

        topicSecondOrderData.setTopicXMLErrors(value);
    }

    @PrePersist
    private void onPrePresist() {
        topicTimeStamp = new Date();
        TopicUtilities.validateAndFixTags(this);
        TopicUtilities.validateAndFixRelationships(this);
        TopicUtilities.updateContentHash(this);
    }

    @PreUpdate
    private void onPreUpdate() {
        TopicUtilities.validateAndFixTags(this);
        TopicUtilities.validateAndFixRelationships(this);
        TopicUtilities.updateContentHash(this);
    }

    @Transient
    public boolean isRelatedTo(final Integer relatedTopicId) {
        for (final TopicToTopic topicToTopic : getParentTopicToTopics())
            if (topicToTopic.getRelatedTopic().topicId.equals(relatedTopicId)) return true;

        return false;
    }

    @Transient
    public boolean isRelatedTo(final Topic relatedTopic, final RelationshipTag relationshipTag) {
        for (final TopicToTopic topicToTopic : getParentTopicToTopics())
            if (topicToTopic.getRelatedTopic().equals(relatedTopic) && topicToTopic.getRelationshipTag().equals(relationshipTag))
                return true;

        return false;
    }

    @Transient
    public boolean isRelatedTo(final Topic relatedTopic) {
        for (final TopicToTopic topicToTopic : getParentTopicToTopics())
            if (topicToTopic.getRelatedTopic().equals(relatedTopic)) return true;

        return false;
    }

    @Transient
    public boolean isTaggedWith(final Integer tagId) {
        for (final TopicToTag topicToTag : getTopicToTags())
            if (topicToTag.getTag().getTagId().equals(tagId)) return true;

        return false;
    }

    @Transient
    public boolean isTaggedWith(final Tag tag) {
        for (final TopicToTag topicToTag : getTopicToTags())
            if (topicToTag.getTag().equals(tag)) return true;

        return false;
    }

    public boolean addRelationshipFrom(final EntityManager entityManager, final Integer topicId, final Integer relationshipTagId) {
        final Topic topic = entityManager.getReference(Topic.class, topicId);
        final RelationshipTag relationshipTag = entityManager.getReference(RelationshipTag.class, relationshipTagId);
        return addRelationshipFrom(topic, relationshipTag);
    }

    public boolean addRelationshipFrom(final EntityManager entityManager, final Topic topic, final Integer relationshipTagId) {
        final RelationshipTag relationshipTag = entityManager.getReference(RelationshipTag.class, relationshipTagId);
        return addRelationshipFrom(topic, relationshipTag);
    }

    public boolean addRelationshipFrom(final Topic relatedTopic, final RelationshipTag relationshipTag) {
        if (!isRelatedTo(relatedTopic, relationshipTag)) {
            final TopicToTopic topicToTopic = new TopicToTopic(relatedTopic, this, relationshipTag);
            addRelationshipFrom(topicToTopic);
            return true;
        }

        return false;
    }

    @Override
    public void addRelationshipFrom(final TopicToTopic relationship) {
        relationship.setRelatedTopic(this);
        getChildTopicToTopics().add(relationship);
        relationship.getRelatedTopic().getParentTopicToTopics().add(relationship);
    }

    public boolean addRelationshipTo(final EntityManager entityManager, final Integer topicId, final Integer relationshipTagId) {
        final Topic topic = entityManager.getReference(Topic.class, topicId);
        final RelationshipTag relationshipTag = entityManager.getReference(RelationshipTag.class, relationshipTagId);
        return addRelationshipTo(topic, relationshipTag);
    }

    public boolean addRelationshipTo(final EntityManager entityManager, final Topic topic, final Integer relationshipTagId) {
        final RelationshipTag relationshipTag = entityManager.getReference(RelationshipTag.class, relationshipTagId);
        return addRelationshipTo(topic, relationshipTag);
    }

    public boolean addRelationshipTo(final Topic relatedTopic, final RelationshipTag relationshipTag) {
        if (!isRelatedTo(relatedTopic, relationshipTag)) {
            final TopicToTopic topicToTopic = new TopicToTopic(this, relatedTopic, relationshipTag);
            addRelationshipTo(topicToTopic);
            return true;
        }

        return false;
    }

    @Override
    public void addRelationshipTo(final TopicToTopic relationship) {
        relationship.setMainTopic(this);
        getParentTopicToTopics().add(relationship);
        relationship.getRelatedTopic().getChildTopicToTopics().add(relationship);
    }

    public void addTag(final EntityManager entityManager, final int tagID) {
        final Tag tag = entityManager.getReference(Tag.class, tagID);
        addTag(tag);
    }

    @Override
    public void addTag(final Tag tag) {
        if (filter(having(on(TopicToTag.class).getTag(), equalTo(tag)), getTopicToTags()).size() == 0) {

            // remove any excluded tags
            for (final Tag excludeTag : tag.getExcludedTags()) {
                if (excludeTag.equals(tag)) continue;

                removeTag(excludeTag);
            }

            // Remove other tags if the category is mutually exclusive
            for (final TagToCategory category : tag.getTagToCategories()) {
                if (category.getCategory().isMutuallyExclusive()) {
                    for (final Tag categoryTag : category.getCategory().getTags()) {
                        if (categoryTag.equals(tag)) continue;

                        // Check if the Category Tag exists in this topic
                        if (filter(having(on(TopicToTag.class).getTag(), equalTo(categoryTag)), getTopicToTags()).size() != 0) {
                            throw new CustomConstraintViolationException(
                                    "Adding Tag " + tag.getTagName() + " (" + tag.getId() + ") failed due to a mutually exclusive " +
                                            "constraint violation.");
                        }
                    }
                }
            }

            final TopicToTag mapping = new TopicToTag(this, tag);
            topicToTags.add(mapping);
            tag.getTopicToTags().add(mapping);
        }
    }

    public void addTopicSourceUrl(final TopicSourceUrl topicSourceUrl) {
        if (filter(having(on(TopicToTopicSourceUrl.class).getTopicSourceUrl(), equalTo(topicSourceUrl)),
                getTopicToTopicSourceUrls()).size() == 0) {
            topicToTopicSourceUrls.add(new TopicToTopicSourceUrl(topicSourceUrl, this));
        }
    }

    public void addBugzillaBug(final BugzillaBug entity) {
        if (filter(having(on(TopicToBugzillaBug.class).getBugzillaBug(), equalTo(entity)), topicToBugzillaBugs).size() == 0) {
            final TopicToBugzillaBug mapping = new TopicToBugzillaBug(entity, this);
            topicToBugzillaBugs.add(mapping);
            entity.getTopicToBugzillaBugs().add(mapping);
        }
    }

    @Transient
    public List<Integer> getIncomingRelatedTopicIDs() {
        final List<Integer> retValue = new ArrayList<Integer>();
        for (final TopicToTopic topicToTopic : getChildTopicToTopics())
            retValue.add(topicToTopic.getMainTopic().getTopicId());
        return retValue;
    }

    @Transient
    public List<Topic> getOutgoingRelatedTopicsArray() {
        final ArrayList<Topic> retValue = new ArrayList<Topic>();
        for (final TopicToTopic topicToTopic : getParentTopicToTopics()) {
            retValue.add(topicToTopic.getRelatedTopic());
        }
        return retValue;
    }

    @Transient
    public List<Topic> getIncomingRelatedTopicsArray() {
        final ArrayList<Topic> retValue = new ArrayList<Topic>();
        for (final TopicToTopic topicToTopic : getChildTopicToTopics())
            retValue.add(topicToTopic.getMainTopic());

        Collections.sort(retValue, new TopicIDComparator());

        return retValue;
    }

    @Transient
    public Topic getRelatedTopicByID(final Integer id) {
        for (final Topic topic : getOutgoingRelatedTopicsArray())
            if (topic.getTopicId().equals(id)) return topic;
        return null;
    }

    @Transient
    public List<Integer> getRelatedTopicIDs() {
        final List<Integer> retValue = new ArrayList<Integer>();
        for (final TopicToTopic topicToTopic : getParentTopicToTopics())
            retValue.add(topicToTopic.getRelatedTopic().getTopicId());
        return retValue;
    }

    @Transient
    public List<Integer> getTagIDs() {
        final List<Integer> retValue = new ArrayList<Integer>();
        for (final TopicToTag topicToTag : topicToTags) {
            final Integer tagId = topicToTag.getTag().getTagId();
            retValue.add(tagId);
        }

        return retValue;
    }

    @Transient
    @Override
    public List<Tag> getTags() {
        final List<Tag> retValue = new ArrayList<Tag>();
        for (final TopicToTag topicToTag : topicToTags) {
            final Tag tag = topicToTag.getTag();
            retValue.add(tag);
        }

        Collections.sort(retValue, new TagIDComparator());

        return retValue;
    }

    @Transient
    public List<Tag> getTagsInCategories(final List<Category> categories) {
        final List<Integer> catgeoriesByID = new ArrayList<Integer>();
        for (final Category category : categories)
            catgeoriesByID.add(category.getCategoryId());
        return getTagsInCategoriesByID(catgeoriesByID);
    }

    @Transient
    public List<Tag> getTagsInCategoriesByID(final List<Integer> categories) {
        final List<Tag> retValue = new ArrayList<Tag>();

        for (final Integer categoryId : categories) {
            for (final TopicToTag topicToTag : topicToTags) {
                final Tag tag = topicToTag.getTag();

                if (topicToTag.getTag().isInCategory(categoryId)) {
                    if (!retValue.contains(tag)) retValue.add(tag);
                }
            }
        }

        return retValue;
    }

    public boolean removeRelationshipTo(final Topic topic, final RelationshipTag relationshipTag) {
        for (final TopicToTopic topicToTopic : getParentTopicToTopics()) {
            final Topic relatedTopic = topicToTopic.getRelatedTopic();
            final RelationshipTag existingRelationshipTag = topicToTopic.getRelationshipTag();

            if (relatedTopic.equals(topic) && existingRelationshipTag.getRelationshipTagId().equals(relationshipTag.getId())) {
                removeRelationshipTo(topicToTopic);
                return true;
            }
        }

        return false;
    }

    public boolean removeRelationshipTo(final Integer relatedTopicId, final Integer relationshipTagId) {
        for (final TopicToTopic topicToTopic : getParentTopicToTopics()) {
            final Topic relatedTopic = topicToTopic.getRelatedTopic();
            final RelationshipTag relationshipTag = topicToTopic.getRelationshipTag();

            if (relatedTopic.getTopicId().equals(relatedTopicId) && relationshipTag.getRelationshipTagId().equals(relationshipTagId)) {
                removeRelationshipTo(topicToTopic);
                return true;
            }
        }

        return false;
    }

    @Override
    public void removeRelationshipTo(final TopicToTopic relationship) {
        getParentTopicToTopics().remove(relationship);
        relationship.getRelatedTopic().getChildTopicToTopics().remove(relationship);
    }

    public boolean removeRelationshipFrom(final Topic topic, final RelationshipTag relationshipTag) {
        for (final TopicToTopic topicToTopic : getChildTopicToTopics()) {
            final Topic relatedTopic = topicToTopic.getRelatedTopic();
            final RelationshipTag existingRelationshipTag = topicToTopic.getRelationshipTag();

            if (relatedTopic.equals(topic) && existingRelationshipTag.getRelationshipTagId().equals(relationshipTag.getId())) {
                removeRelationshipFrom(topicToTopic);
                return true;
            }
        }

        return false;
    }

    public boolean removeRelationshipFrom(final Integer relatedTopicId, final Integer relationshipTagId) {
        for (final TopicToTopic topicToTopic : getChildTopicToTopics()) {
            final Topic relatedTopic = topicToTopic.getRelatedTopic();
            final RelationshipTag relationshipTag = topicToTopic.getRelationshipTag();

            if (relatedTopic.getTopicId().equals(relatedTopicId) && relationshipTag.getRelationshipTagId().equals(relationshipTagId)) {
                removeRelationshipFrom(topicToTopic);
                return true;
            }
        }

        return false;
    }

    @Override
    public void removeRelationshipFrom(final TopicToTopic relationship) {
        getChildTopicToTopics().remove(relationship);
        relationship.getMainTopic().getParentTopicToTopics().remove(relationship);
    }

    public void removeTag(final int tagID) {
        final List<TopicToTag> mappingEntities = filter(having(on(TopicToTag.class).getTag().getTagId(), equalTo(tagID)), getTopicToTags());
        if (mappingEntities.size() != 0) {
            for (final TopicToTag mapping : mappingEntities) {
                topicToTags.remove(mapping);
                mapping.getTag().getTopicToTags().remove(mapping);
            }
        }
    }

    @Override
    public void removeTag(final Tag tag) {
        removeTag(tag.getTagId());
    }

    public void removeTopicSourceUrl(final TopicSourceUrl topicSourceUrl) {
        removeTopicSourceUrl(topicSourceUrl.getId());
    }

    public void removeTopicSourceUrl(final int id) {
        final List<TopicToTopicSourceUrl> mappingEntities = filter(
                having(on(TopicToTopicSourceUrl.class).getTopicSourceUrl().getTopicSourceUrlId(), equalTo(id)),
                getTopicToTopicSourceUrls());
        if (mappingEntities.size() != 0) {
            for (final TopicToTopicSourceUrl mapping : mappingEntities) {
                topicToTopicSourceUrls.remove(mapping);
            }
        }
    }

    public void removeBugzillaBug(final int id) {
        final List<TopicToBugzillaBug> mappingEntities = filter(
                having(on(TopicToBugzillaBug.class).getBugzillaBug().getBugzillaBugId(), equalTo(id)), topicToBugzillaBugs);
        if (mappingEntities.size() != 0) {
            for (final TopicToBugzillaBug mapping : mappingEntities) {
                topicToBugzillaBugs.remove(mapping);
                mapping.getBugzillaBug().getTopicToBugzillaBugs().remove(mapping);
            }
        }
    }

    @Transient
    public List<TopicToTopic> getParentTopicToTopicsArray() {
        final List<TopicToTopic> retValue = CollectionUtilities.toArrayList(parentTopicToTopics);
        Collections.sort(retValue, new TopicToTopicRelatedTopicIDSort());
        return retValue;
    }

    @Transient
    public List<TopicToTopic> getChildTopicToTopicsArray() {
        final List<TopicToTopic> retValue = CollectionUtilities.toArrayList(childTopicToTopics);
        Collections.sort(retValue, new TopicToTopicMainTopicIDSort());
        return retValue;
    }

    public void changeTopicToTopicRelationshipTag(final RelationshipTag relationshipTag, final Topic existingTopic,
            final RelationshipTag existingRelationshipTag) {
        for (final TopicToTopic topicToTopic : parentTopicToTopics) {
            if (topicToTopic.getRelatedTopic().equals(existingTopic) && topicToTopic.getRelationshipTag().equals(existingRelationshipTag)) {
                topicToTopic.setRelationshipTag(relationshipTag);
                break;
            }
        }
    }

    @Transient
    public List<TopicSourceUrl> getTopicSourceUrls() {
        final List<TopicSourceUrl> retValue = new ArrayList<TopicSourceUrl>();
        for (final TopicToTopicSourceUrl mapping : topicToTopicSourceUrls) {
            final TopicSourceUrl entity = mapping.getTopicSourceUrl();
            retValue.add(entity);
        }

        return retValue;
    }

    @Transient
    public List<BugzillaBug> getBugzillaBugs() {
        final List<BugzillaBug> retValue = new ArrayList<BugzillaBug>();
        for (final TopicToBugzillaBug mapping : topicToBugzillaBugs) {
            final BugzillaBug entity = mapping.getBugzillaBug();
            retValue.add(entity);
        }

        return retValue;
    }

    @Override
    @Transient
    public Set<TopicToPropertyTag> getPropertyTags() {
        return topicToPropertyTags;
    }

    @Override
    public void removePropertyTag(final TopicToPropertyTag topicToPropertyTag) {
        topicToPropertyTags.remove(topicToPropertyTag);
        topicToPropertyTag.getPropertyTag().getTopicToPropertyTags().remove(topicToPropertyTag);
    }

    @Override
    public void addPropertyTag(final TopicToPropertyTag topicToPropertyTag) {
        topicToPropertyTag.setTopic(this);
        topicToPropertyTags.add(topicToPropertyTag);
        topicToPropertyTag.getPropertyTag().getTopicToPropertyTags().add(topicToPropertyTag);
    }

    @PreRemove
    private void preRemove() {
        for (final TopicToTag mapping : topicToTags)
            mapping.getTag().getTopicToTags().remove(mapping);

        for (final TopicToTopic mapping : childTopicToTopics)
            mapping.getMainTopic().getParentTopicToTopics().remove(mapping);

        for (final TopicToTopic mapping : parentTopicToTopics)
            mapping.getRelatedTopic().getChildTopicToTopics().remove(mapping);

        for (final TopicToPropertyTag mapping : topicToPropertyTags)
            mapping.getPropertyTag().getTopicToPropertyTags().remove(mapping);

        for (final TopicToTopicSourceUrl mapping : topicToTopicSourceUrls)
            mapping.getTopicSourceUrl().getTopicToTopicSourceUrls().remove(mapping);

        for (final TopicToBugzillaBug mapping : topicToBugzillaBugs)
            mapping.getBugzillaBug().getTopicToBugzillaBugs().remove(mapping);

        topicToTags.clear();
        childTopicToTopics.clear();
        parentTopicToTopics.clear();
        topicToPropertyTags.clear();
        topicToTopicSourceUrls.clear();
        topicToBugzillaBugs.clear();
    }

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

        topicToPropertyTags.add(mapping);
        propertyTag.getTopicToPropertyTags().add(mapping);
    }

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

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

        for (final TopicToPropertyTag mapping : removeList) {
            topicToPropertyTags.remove(mapping);
            mapping.getPropertyTag().getTopicToPropertyTags().remove(mapping);
        }
    }

    @Transient
    public void addMinHash(final MinHash minHash) {
        minHashes.add(minHash);
        minHash.setTopic(this);
    }

    @Transient
    public void removeMinHash(final MinHash minHash) {
        if (minHashes.contains(minHash)) {
            minHashes.remove(minHash);
            minHash.setTopic(null);
        }
    }

    @SuppressWarnings("unchecked")
    @Transient
    public List<TranslatedTopicData> getTranslatedTopics(final EntityManager entityManager, final Number revision) {
        final List<TranslatedTopicData> translatedTopicDatas = new ArrayList<TranslatedTopicData>();

        /*
         * We have to do a query here as a @OneToMany won't work with hibernate envers since the TranslatedTopic entity is
         * audited and we need the latest results. This is because the translated topic will never exist for its matching
         * audited topic.
         */
        final String translatedTopicQuery = TranslatedTopic.SELECT_ALL_QUERY + " WHERE translatedTopic.topicId = " + topicId +
                (revision == null ? "" : (" AND translatedTopic.topicRevision <= " + revision));
        final List<TranslatedTopic> translatedTopics = entityManager.createQuery(translatedTopicQuery).getResultList();

        for (final TranslatedTopic translatedTopic : translatedTopics) {
            translatedTopicDatas.addAll(translatedTopic.getTranslatedTopicDatas());
        }

        return translatedTopicDatas;
    }

    @Transient
    public List<ContentSpec> getContentSpecs(final EntityManager entityManager) {
        final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        final CriteriaQuery<ContentSpec> query = criteriaBuilder.createQuery(ContentSpec.class);
        final Root<CSNode> root = query.from(CSNode.class);
        query.select(root.get("contentSpec").as(ContentSpec.class));
        query.distinct(true);

        // Regular topics
        final Predicate topicIdMatches = criteriaBuilder.equal(root.get("entityId"), getTopicId());
        final Predicate normalTopicMatch = criteriaBuilder.equal(root.get("CSNodeType"), CommonConstants.CS_NODE_TOPIC);
        final Predicate levelTopicMatch = criteriaBuilder.equal(root.get("CSNodeType"), CommonConstants.CS_NODE_INITIAL_CONTENT_TOPIC);
        final Predicate metaDataTopicMatch = criteriaBuilder.equal(root.get("CSNodeType"), CommonConstants.CS_NODE_META_DATA_TOPIC);
        final Predicate topicTypeMatches = criteriaBuilder.or(normalTopicMatch, levelTopicMatch, metaDataTopicMatch);

        // Info topics
        final Join<CSInfoNode, CSNode> csInfoNode = root.join("CSInfoNode", JoinType.LEFT);
        final Predicate infoTopicIdMatches = criteriaBuilder.equal(csInfoNode.get("topicId"), getTopicId());

        final Predicate normalTopicMatches = criteriaBuilder.and(topicIdMatches, topicTypeMatches);
        query.where(criteriaBuilder.or(normalTopicMatches, infoTopicIdMatches));

        final List<ContentSpec> results = entityManager.createQuery(query).getResultList();
        return results;
    }
}


/*-
 * #%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;

/*
 * 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 org.apache.maven.doxia.sink.Sink;

import java.util.ResourceBundle;

/**
 * A little tool to deal with info/warning/error icons in Checkstyle reports, with eventual text.
 *
 * @since 2.13
 */
public class IconTool {

    public static final String INFO = "info";
    public static final String WARNING = "warning";
    public static final String ERROR = "error";

    public static final int NO_TEXT = 0;
    public static final int TEXT_SIMPLE = 1;
    public static final int TEXT_TITLE = 2;
    public static final int TEXT_ABBREV = 3;

    private final Sink sink;

    private final ResourceBundle bundle;

    public IconTool(Sink sink, ResourceBundle bundle) {
        this.sink = sink;
        this.bundle = bundle;
    }

    /**
     * Render a simple icon of given level.
     *
     * @param level one of INFO, WARNING or ERROR constants
     */
    public void iconSeverity(String level) {
        sink.figure();
        sink.figureGraphics("images/icon_" + level + "_sml.gif");
        sink.figure_();
    }

    /**
     * Render an icon of given level with associated text.
     *
     * @param level    one of INFO, WARNING or ERROR constants
     * @param textType one of NO_TEXT, TEXT_SIMPLE, TEXT_TITLE or
     *                 TEXT_ABBREV constants
     */
    public void iconSeverity(String level, int textType) {
        sink.figure();
        sink.figureGraphics("images/icon_" + level + "_sml.gif");
        sink.figure_();

        if (textType > 0) {
            sink.nonBreakingSpace();

            sink.text(bundle.getString("report.checkstyle." + level + suffix(textType)));
        }
    }

    /**
     * Render an info icon.
     */
    public void iconInfo() {
        iconSeverity(INFO);
    }

    /**
     * Render an info icon with associated text.
     *
     * @param textType one of NO_TEXT, TEXT_SIMPLE, TEXT_TITLE or
     *                 TEXT_ABBREV constants
     */
    public void iconInfo(int textType) {
        iconSeverity(INFO, textType);
    }

    /**
     * Render a warning icon.
     */
    public void iconWarning() {
        iconSeverity(WARNING);
    }

    /**
     * Render a warning icon with associated text.
     *
     * @param textType one of NO_TEXT, TEXT_SIMPLE, TEXT_TITLE or
     *                 TEXT_ABBREV constants
     */
    public void iconWarning(int textType) {
        iconSeverity(WARNING, textType);
    }

    /**
     * Render an error icon.
     */
    public void iconError() {
        iconSeverity(ERROR);
    }

    /**
     * Render an error icon with associated text.
     *
     * @param textType one of NO_TEXT, TEXT_SIMPLE, TEXT_TITLE or
     *                 TEXT_ABBREV constants
     */
    public void iconError(int textType) {
        iconSeverity(ERROR, textType);
    }

    private String suffix(int textType) {
        switch (textType) {
            case TEXT_TITLE:
                return "s";
            case TEXT_ABBREV:
                return "s.abbrev";
            default:
                return "";
        }
    }
}


/*
  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;

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.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.PreRemove;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;

import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.envers.Audited;
import org.hibernate.validator.constraints.NotBlank;
import org.jboss.pressgang.ccms.model.base.AuditedEntity;
import org.jboss.pressgang.ccms.model.constants.Constants;
import org.jboss.pressgang.ccms.model.sort.TagIDComparator;
import org.jboss.pressgang.ccms.utils.structures.NameIDSortMap;

@Audited
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Table(name = "Project", uniqueConstraints = @UniqueConstraint(columnNames = {"ProjectName"}))
public class Project extends AuditedEntity implements java.io.Serializable {
    public static final String SELECT_ALL_QUERY = "select project from Project project";
    private static final long serialVersionUID = 7468160102030564523L;

    private Integer projectId;
    private String projectName;
    private String projectDescription;
    private Set<TagToProject> tagToProjects = new HashSet<TagToProject>(0);

    public Project() {
    }

    public Project(final String projectName) {
        this.projectName = projectName;
    }

    public Project(final String projectName, final String projectDescription, final Set<TagToProject> tagToProjects) {
        this.projectName = projectName;
        this.projectDescription = projectDescription;
        this.tagToProjects = tagToProjects;
    }

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "ProjectID", unique = true, nullable = false)
    public Integer getProjectId() {
        return projectId;
    }

    public void setProjectId(final Integer projectId) {
        this.projectId = projectId;
    }

    @Column(name = "ProjectName", nullable = false, length = 255)
    @NotNull(message = "{project.name.notBlank}")
    @NotBlank(message = "{project.name.notBlank}")
    @Size(max = 255)
    public String getProjectName() {
        return projectName;
    }

    public void setProjectName(final String projectName) {
        this.projectName = projectName;
    }

    @Column(name = "ProjectDescription", columnDefinition = "TEXT")
    @Size(max = 65535)
    public String getProjectDescription() {
        return projectDescription;
    }

    public void setProjectDescription(final String projectDescription) {
        this.projectDescription = projectDescription;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "project",
            cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, orphanRemoval = true)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<TagToProject> getTagToProjects() {
        return tagToProjects;
    }

    public void setTagToProjects(final Set<TagToProject> tagToProjects) {
        this.tagToProjects = tagToProjects;
    }

    @Transient
    public String getTagsList() {
        return getTagsList(true);
    }

    /**
     * Generates a HTML formatted and categorized list of the tags that are associated with this topic
     *
     * @return A HTML String to display in a table
     */
    @Transient
    public String getTagsList(final boolean brLineBreak) {
        // define the line breaks for html and for tooltips
        final String lineBreak = brLineBreak ? "
" : "\n";
        final String boldStart = brLineBreak ? "" : "";
        final String boldEnd = brLineBreak ? "" : "";

        final TreeMap<NameIDSortMap, ArrayList<String>> tags = new TreeMap<NameIDSortMap, ArrayList<String>>();

        for (final TagToProject tagToProject : tagToProjects) {
            final Tag tag = tagToProject.getTag();
            final Set<TagToCategory> tagToCategories = tag.getTagToCategories();

            if (tagToCategories.size() == 0) {
                NameIDSortMap categoryDetails = new NameIDSortMap("Uncategorised", -1, 0);

                if (!tags.containsKey(categoryDetails)) tags.put(categoryDetails, new ArrayList<String>());

                tags.get(categoryDetails).add(tag.getTagName());
            } else {
                for (final TagToCategory category : tagToCategories) {
                    NameIDSortMap categoryDetails = new NameIDSortMap(category.getCategory().getCategoryName(),
                            category.getCategory().getCategoryId(),
                            category.getCategory().getCategorySort() == null ? 0 : category.getCategory().getCategorySort());

                    if (!tags.containsKey(categoryDetails)) tags.put(categoryDetails, new ArrayList<String>());

                    tags.get(categoryDetails).add(tag.getTagName());
                }
            }
        }

        String tagsList = "";
        for (final NameIDSortMap key : tags.keySet()) {
            // sort alphabetically
            Collections.sort(tags.get(key));

            if (tagsList.length() != 0) tagsList += lineBreak;

            tagsList += boldStart + key.getName() + boldEnd + ": ";

            String thisTagList = "";

            for (final String tag : tags.get(key)) {
                if (thisTagList.length() != 0) thisTagList += ", ";

                thisTagList += tag;
            }

            tagsList += thisTagList + " ";
        }

        return tagsList;
    }

    @Transient
    public List<Tag> getTags() {
        final List<Tag> retValue = new ArrayList<Tag>();
        for (final TagToProject tag : tagToProjects)
            retValue.add(tag.getTag());

        Collections.sort(retValue, new TagIDComparator());

        return retValue;
    }

    @PreRemove
    private void preRemove() {
        for (final TagToProject mapping : getTagToProjects())
            mapping.getTag().getTagToProjects().remove(mapping);

        tagToProjects.clear();
    }

    @Transient
    public boolean isRelatedTo(final Tag tag) {
        for (final TagToProject mapping : getTagToProjects())
            if (mapping.getTag().equals(tag)) return true;

        return false;
    }

    public boolean addRelationshipTo(final Tag tag) {
        if (!isRelatedTo(tag)) {
            final TagToProject mapping = new TagToProject(this, tag);
            getTagToProjects().add(mapping);
            tag.getTagToProjects().add(mapping);
            return true;
        }

        return false;
    }

    public boolean removeRelationshipTo(final Integer relatedTagId) {
        for (final TagToProject mapping : getTagToProjects()) {
            final Tag tag = mapping.getTag();

            if (tag.getTagId().equals(relatedTagId)) {
                /* remove the relationship from this project */
                getTagToProjects().remove(mapping);

                /* remove the relationship from the tag */
                tag.getTagToProjects().remove(mapping);

                return true;
            }
        }

        return false;
    }

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


/*-
 * #%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;

/*
 * 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.DefaultLogger;
import com.puppycrawl.tools.checkstyle.XMLLogger;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.doxia.tools.SiteTool;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginManagement;
import org.apache.maven.model.ReportPlugin;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
import org.codehaus.plexus.resource.ResourceManager;
import org.codehaus.plexus.resource.loader.FileResourceLoader;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.PathTool;
import org.codehaus.plexus.util.StringUtils;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleExecutor;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleExecutorException;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleExecutorRequest;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleResults;
import se.jguru.nazgul.tools.plugin.checkstyle.rss.CheckstyleRssGenerator;
import se.jguru.nazgul.tools.plugin.checkstyle.rss.CheckstyleRssGeneratorRequest;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

/**
 * Base abstract class for Checkstyle reports.
 *
 * @version $Id$
 */
public abstract class AbstractCheckstyleReport extends AbstractMavenReport {
    public static final String PLUGIN_RESOURCES = "org/apache/maven/plugin/checkstyle";

    protected static final String JAVA_FILES = "**\\/*.java";

    /**
     * Specifies the cache file used to speed up Checkstyle on successive runs.
     */
    @Parameter(defaultValue = "${project.build.directory}/checkstyle-cachefile")
    protected String cacheFile;

    /**
     *

     * Specifies the location of the XML configuration to use.
     *

     *

     *

     * Potential values are a filesystem path, a URL, or a classpath resource.
     * This parameter expects that the contents of the location conform to the
     * xml format (Checkstyle Checker
     * module) configuration of rulesets.
     *

     *

     *

     * This parameter is resolved as resource, URL, then file. If successfully
     * resolved, the contents of the configuration is copied into the
     * ${project.build.directory}/checkstyle-configuration.xml
     * file before being passed to Checkstyle as a configuration.
     *

     *

     *

     * There are 2 predefined rulesets included in Maven Checkstyle Plugin:
     *

     *

     *
sun_checks.xml: Sun Checks.

     *
google_checks.xml: Google Checks.

     *

     */
    @Parameter(property = "checkstyle.config.location", defaultValue = "sun_checks.xml")
    protected String configLocation;

    /**
     * Output errors to console.
     */
    @Parameter(property = "checkstyle.consoleOutput", defaultValue = "false")
    protected boolean consoleOutput;

    /**
     * The file encoding to use when reading the source files. If the property project.build.sourceEncoding
     * is not set, the platform default encoding is used. Note: This parameter always overrides the
     * property charset from Checkstyle's TreeWalker module.
     *
     * @since 2.2
     */
    @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
    protected String encoding;

    /**
     * Specifies if the build should fail upon a violation.
     */
    @Parameter(defaultValue = "false")
    protected boolean failsOnError;

    /**
     *

     * Specifies the location of the License file (a.k.a. the header file) that
     * can be used by Checkstyle to verify that source code has the correct
     * license header.
     *

     *

     * You need to use ${checkstyle.header.file} in your Checkstyle xml
     * configuration to reference the name of this header file.
     *

     *

     * For instance:
     *

     *

     *
     * <module name="RegexpHeader">
     * <property name="headerFile" value="${checkstyle.header.file}"/>
     * </module>
     *
     *

     *
     * @since 2.0-beta-2
     */
    @Parameter(property = "checkstyle.header.file", defaultValue = "LICENSE.txt")
    protected String headerLocation;

    /**
     * Skip entire check.
     *
     * @since 2.2
     */
    @Parameter(property = "checkstyle.skip", defaultValue = "false")
    protected boolean skip;

    /**
     * Specifies the path and filename to save the Checkstyle output. The format
     * of the output file is determined by the outputFileFormat
     * parameter.
     */
    @Parameter(property = "checkstyle.output.file", defaultValue = "${project.build.directory}/checkstyle-result.xml")
    private File outputFile;

    /**
     *

     * Specifies the location of the properties file.
     *

     *

     *

     * This parameter is resolved as URL, File then resource. If successfully
     * resolved, the contents of the properties location is copied into the
     * ${project.build.directory}/checkstyle-checker.properties
     * file before being passed to Checkstyle for loading.
     *

     *

     *

     * The contents of the propertiesLocation will be made
     * available to Checkstyle for specifying values for parameters within the
     * xml configuration (specified in the configLocation
     * parameter).
     *

     *
     * @since 2.0-beta-2
     */
    @Parameter(property = "checkstyle.properties.location")
    protected String propertiesLocation;

    /**
     * Allows for specifying raw property expansion information.
     */
    @Parameter
    protected String propertyExpansion;

    /**
     * Specifies the location of the resources to be used for Checkstyle.
     *
     * @since 2.10
     */
    @Parameter(defaultValue = "${project.resources}", readonly = true)
    protected List<Resource> resources;

    /**
     * Specifies the location of the test resources to be used for Checkstyle.
     *
     * @since 2.11
     */
    @Parameter(defaultValue = "${project.testResources}", readonly = true)
    protected List<Resource> testResources;

    /**
     * Specifies the names filter of the source files to be used for Checkstyle.
     */
    @Parameter(property = "checkstyle.includes", defaultValue = JAVA_FILES, required = true)
    protected String includes;

    /**
     * Specifies the names filter of the source files to be excluded for
     * Checkstyle.
     */
    @Parameter(property = "checkstyle.excludes")
    protected String excludes;

    /**
     * Specifies the names filter of the resource files to be used for Checkstyle.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.resourceIncludes", defaultValue = "**/*.properties", required = true)
    protected String resourceIncludes;

    /**
     * Specifies the names filter of the resource files to be excluded for
     * Checkstyle.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.resourceExcludes")
    protected String resourceExcludes;

    /**
     * Specifies whether to include the resource directories in the check.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.includeResources", defaultValue = "true", required = true)
    protected boolean includeResources;

    /**
     * Specifies whether to include the test resource directories in the check.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.includeTestResources", defaultValue = "true", required = true)
    protected boolean includeTestResources;

    /**
     * Specifies the location of the source directory to be used for Checkstyle.
     *
     * @deprecated instead use {@link #sourceDirectories}
     */
    @Deprecated
    @Parameter
    private File sourceDirectory;

    /**
     * Specifies the location of the source directories to be used for Checkstyle.
     *
     * @since 2.13
     */
    @Parameter(defaultValue = "${project.compileSourceRoots}")
    private List<String> sourceDirectories;

    /**
     * Specifies the location of the test source directory to be used for
     * Checkstyle.
     *
     * @since 2.2
     * @deprecated instead use {@link #testSourceDirectories}
     */
    @Parameter
    @Deprecated
    private File testSourceDirectory;

    /**
     * Specifies the location of the test source directories to be used for Checkstyle.
     *
     * @since 2.13
     */
    @Parameter(defaultValue = "${project.testCompileSourceRoots}")
    private List<String> testSourceDirectories;

    /**
     * Include or not the test source directory/directories to be used for Checkstyle.
     *
     * @since 2.2
     */
    @Parameter(defaultValue = "false")
    protected boolean includeTestSourceDirectory;

    /**
     * The key to be used in the properties for the suppressions file.
     *
     * @since 2.1
     */
    @Parameter(property = "checkstyle.suppression.expression", defaultValue = "checkstyle.suppressions.file")
    protected String suppressionsFileExpression;

    /**
     *

     * Specifies the location of the suppressions XML file to use.
     *

     *

     *

     * This parameter is resolved as resource, URL, then file. If successfully
     * resolved, the contents of the suppressions XML is copied into the
     * ${project.build.directory}/checkstyle-supressions.xml file
     * before being passed to Checkstyle for loading.
     *

     *

     *

     * See suppressionsFileExpression for the property that will
     * be made available to your Checkstyle configuration.
     *

     *
     * @since 2.0-beta-2
     */
    @Parameter(property = "checkstyle.suppressions.location")
    protected String suppressionsLocation;

    /**
     * If null, the Checkstyle plugin will display violations on stdout.
     * Otherwise, a text file will be created with the violations.
     */
    @Parameter
    private File useFile;

    /**
     * Specifies the format of the output to be used when writing to the output
     * file. Valid values are "plain" and "xml".
     */
    @Parameter(property = "checkstyle.output.format", defaultValue = "xml")
    private String outputFileFormat;

    /**
     * Specifies if the Rules summary should be enabled or not.
     */
    @Parameter(property = "checkstyle.enable.rules.summary", defaultValue = "true")
    private boolean enableRulesSummary;

    /**
     * Specifies if the Severity summary should be enabled or not.
     */
    @Parameter(property = "checkstyle.enable.severity.summary", defaultValue = "true")
    private boolean enableSeveritySummary;

    /**
     * Specifies if the Files summary should be enabled or not.
     */
    @Parameter(property = "checkstyle.enable.files.summary", defaultValue = "true")
    private boolean enableFilesSummary;

    /**
     * Specifies if the RSS should be enabled or not.
     */
    @Parameter(property = "checkstyle.enable.rss", defaultValue = "true")
    private boolean enableRSS;

    /**
     * SiteTool.
     *
     * @since 2.2
     */
    @Component(role = SiteTool.class)
    protected SiteTool siteTool;

    /**
     * The Plugin Descriptor
     */
    @Parameter(defaultValue = "${plugin}", readonly = true, required = true)
    private PluginDescriptor plugin;

    // remove when requiring Maven 3.x, just use #plugin
    @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
    private MojoExecution mojoExecution;

    /**
     * Link the violation line numbers to the source xref. Will link
     * automatically if Maven JXR plugin is being used.
     *
     * @since 2.1
     */
    @Parameter(property = "linkXRef", defaultValue = "true")
    private boolean linkXRef;

    /**
     * Location of the Xrefs to link to.
     */
    @Parameter(defaultValue = "${project.reporting.outputDirectory}/xref")
    private File xrefLocation;

    /**
     * When using custom treeWalkers, specify their names here so the checks
     * inside the treeWalker end up the the rule-summary.
     *
     * @since 2.11
     */
    @Parameter
    private List<String> treeWalkerNames;

    /**
     */
    @Component
    protected ResourceManager locator;

    /**
     * CheckstyleRssGenerator.
     *
     * @since 2.4
     */
    @Component(role = CheckstyleRssGenerator.class, hint = "default")
    protected CheckstyleRssGenerator checkstyleRssGenerator;

    /**
     * @since 2.5
     */
    @Component(role = CheckstyleExecutor.class, hint = "default")
    protected CheckstyleExecutor checkstyleExecutor;

    protected ByteArrayOutputStream stringOutputStream;

    /**
     * {@inheritDoc}
     */
    public String getName(final Locale locale) {
        return getBundle(locale).getString("report.checkstyle.name");
    }

    /**
     * {@inheritDoc}
     */
    public String getDescription(final Locale locale) {
        return getBundle(locale).getString("report.checkstyle.description");
    }

    /**
     * {@inheritDoc}
     */
    public void executeReport(final Locale locale)
            throws MavenReportException {
        locator.addSearchPath(FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath());
        locator.addSearchPath("url", "");

        locator.setOutputDirectory(new File(project.getBuild().getDirectory()));

        // for when we start using maven-shared-io and maven-shared-monitor...
        // locator = new Locator( new MojoLogMonitorAdaptor( getLog() ) );

        // locator = new Locator( getLog(), new File( project.getBuild().getDirectory() ) );

        ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();

        try {
            CheckstyleExecutorRequest request = createRequest()
                    .setLicenseArtifacts(collectArtifacts("license"))
                    .setConfigurationArtifacts(collectArtifacts("configuration"));

            CheckstyleResults results = checkstyleExecutor.executeCheckstyle(request);

            ResourceBundle bundle = getBundle(locale);
            generateReportStatics();
            generateMainReport(results, bundle);
            if (enableRSS) {
                CheckstyleRssGeneratorRequest checkstyleRssGeneratorRequest =
                        new CheckstyleRssGeneratorRequest(this.project, this.getCopyright(), outputDirectory, getLog());
                checkstyleRssGenerator.generateRSS(results, checkstyleRssGeneratorRequest);
            }

        } catch (CheckstyleException e) {
            throw new MavenReportException("Failed during checkstyle configuration", e);
        } catch (CheckstyleExecutorException e) {
            throw new MavenReportException("Failed during checkstyle execution", e);
        } finally {
            //be sure to restore original context classloader
            Thread.currentThread().setContextClassLoader(currentClassLoader);
        }
    }

    /**
     * Create the Checkstyle executor request.
     *
     * @return The executor request.
     * @throws MavenReportException If something goes wrong during creation.
     */
    protected abstract CheckstyleExecutorRequest createRequest()
            throws MavenReportException;

    @SuppressWarnings("unchecked")
    private List<Artifact> collectArtifacts(final String hint) {
        if (plugin == null || plugin.getGroupId() == null) {
            // Maven 2.x workaround
            plugin = mojoExecution.getMojoDescriptor().getPluginDescriptor();
        }

        List<Artifact> artifacts = new ArrayList<>();

        PluginManagement pluginManagement = project.getBuild().getPluginManagement();
        if (pluginManagement != null) {
            artifacts.addAll(getCheckstylePluginDependenciesAsArtifacts(pluginManagement.getPluginsAsMap(), hint));
        }

        artifacts.addAll(getCheckstylePluginDependenciesAsArtifacts(project.getBuild().getPluginsAsMap(), hint));

        return artifacts;
    }

    private List<Artifact> getCheckstylePluginDependenciesAsArtifacts(final Map<String, Plugin> plugins,
                                                                      final String hint) {
        List<Artifact> artifacts = new ArrayList<>();

        Plugin checkstylePlugin = plugins.get(plugin.getGroupId() + ":" + plugin.getArtifactId());
        if (checkstylePlugin != null) {
            for (Dependency dep : checkstylePlugin.getDependencies()) {
                // @todo if we can filter on hints, it should be done here...
                String depKey = dep.getGroupId() + ":" + dep.getArtifactId();
                final Artifact artifact = (Artifact) plugin.getArtifactMap().get(depKey);

                if (artifact != null) {
                    artifacts.add(artifact);
                } else if (getLog().isDebugEnabled()) {
                    getLog().debug("Got null CheckstylePlugin Dependency Artifact from key ["
                            + depKey + "]. Excluding null Artifact from return List [" + hint + "]. ");
                }
            }
        }
        return artifacts;
    }

    /**
     * Creates and returns the report generation listener.
     *
     * @return The audit listener.
     * @throws MavenReportException If something goes wrong.
     */
    protected AuditListener getListener()
            throws MavenReportException {
        AuditListener listener = null;

        if (StringUtils.isNotEmpty(outputFileFormat)) {
            File resultFile = outputFile;

            OutputStream out = getOutputStream(resultFile);

            if ("xml".equals(outputFileFormat)) {
                listener = new XMLLogger(out, true);
            } else if ("plain".equals(outputFileFormat)) {
                listener = new DefaultLogger(out, true);
            } else {
                // TODO: failure if not a report
                throw new MavenReportException("Invalid output file format: (" + outputFileFormat
                        + "). Must be 'plain' or 'xml'.");
            }
        }

        return listener;
    }

    private OutputStream getOutputStream(final File file)
            throws MavenReportException {
        File parentFile = file.getAbsoluteFile().getParentFile();

        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }

        FileOutputStream fileOutputStream;
        try {
            fileOutputStream = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            throw new MavenReportException("Unable to create output stream: " + file, e);
        }
        return fileOutputStream;
    }

    /**
     * Creates and returns the console listener.
     *
     * @return The console listener.
     * @throws MavenReportException If something goes wrong.
     */
    protected DefaultLogger getConsoleListener()
            throws MavenReportException {
        DefaultLogger consoleListener;

        if (useFile == null) {
            stringOutputStream = new ByteArrayOutputStream();
            consoleListener = new DefaultLogger(stringOutputStream, false);
        } else {
            OutputStream out = getOutputStream(useFile);

            consoleListener = new DefaultLogger(out, true);
        }

        return consoleListener;
    }

    private void generateReportStatics()
            throws MavenReportException {
        ReportResource rresource = new ReportResource(PLUGIN_RESOURCES, outputDirectory);
        try {
            rresource.copy("images/rss.png");
        } catch (IOException e) {
            throw new MavenReportException("Unable to copy static resources.", e);
        }
    }

    private String getCopyright() {
        String copyright;
        int currentYear = Calendar.getInstance().get(Calendar.YEAR);
        if (StringUtils.isNotEmpty(project.getInceptionYear())
                && !String.valueOf(currentYear).equals(project.getInceptionYear())) {
            copyright = project.getInceptionYear() + " - " + currentYear;
        } else {
            copyright = String.valueOf(currentYear);
        }

        if ((project.getOrganization() != null) && StringUtils.isNotEmpty(project.getOrganization().getName())) {
            copyright = copyright + " " + project.getOrganization().getName();
        }
        return copyright;
    }

    private void generateMainReport(final CheckstyleResults results, final ResourceBundle bundle) {
        CheckstyleReportGenerator generator =
                new CheckstyleReportGenerator(getSink(), bundle, project.getBasedir(), siteTool, configLocation);

        generator.setLog(getLog());
        generator.setEnableRulesSummary(enableRulesSummary);
        generator.setEnableSeveritySummary(enableSeveritySummary);
        generator.setEnableFilesSummary(enableFilesSummary);
        generator.setEnableRSS(enableRSS);
        generator.setCheckstyleConfig(results.getConfiguration());
        if (linkXRef) {
            String relativePath = PathTool.getRelativePath(getOutputDirectory(), xrefLocation.getAbsolutePath());
            if (StringUtils.isEmpty(relativePath)) {
                relativePath = ".";
            }
            relativePath = relativePath + "/" + xrefLocation.getName();
            if (xrefLocation.exists()) {
                // XRef was already generated by manual execution of a lifecycle
                // binding
                generator.setXrefLocation(relativePath);
            } else {
                // Not yet generated - check if the report is on its way
                for (ReportPlugin report : (Iterable<ReportPlugin>) getProject().getReportPlugins()) {
                    String artifactId = report.getArtifactId();
                    if ("maven-jxr-plugin".equals(artifactId) || "jxr-maven-plugin".equals(artifactId)) {
                        generator.setXrefLocation(relativePath);
                    }
                }
            }

            if (generator.getXrefLocation() == null && results.getFileCount() > 0) {
                getLog().warn("Unable to locate Source XRef to link to - DISABLED");
            }
        }
        if (treeWalkerNames != null) {
            generator.setTreeWalkerNames(treeWalkerNames);
        }
        generator.generateReport(results);
    }

    private static ResourceBundle getBundle(Locale locale) {
        return ResourceBundle.getBundle("checkstyle-report", locale, AbstractCheckstyleReport.class.getClassLoader());
    }

    protected List<File> getSourceDirectories() {
        List<File> sourceDirs = null;
        // if sourceDirectory is explicitly set, use it
        if (sourceDirectory != null) {
            sourceDirs = Collections.singletonList(sourceDirectory);
        } else {
            sourceDirs = new ArrayList<>(sourceDirectories.size());
            for (String sourceDir : sourceDirectories) {
                sourceDirs.add(FileUtils.resolveFile(project.getBasedir(), sourceDir));
            }
        }

        return sourceDirs;
    }

    protected List<File> getTestSourceDirectories() {
        List<File> testSourceDirs = null;
        // if testSourceDirectory is explicitly set, use it
        if (testSourceDirectory != null) {
            testSourceDirs = Collections.singletonList(testSourceDirectory);
        }

        // probably null-check only required due to MavenProjectStubs
        else if (testSourceDirectories != null) {
            testSourceDirs = new ArrayList<>(testSourceDirectories.size());
            for (String testSourceDir : testSourceDirectories) {
                testSourceDirs.add(FileUtils.resolveFile(project.getBasedir(), testSourceDir));
            }
        }

        return testSourceDirs;
    }
}