/*-
 * #%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 java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ResourceBundle;

import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributeSet;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.tools.SiteTool;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.codehaus.plexus.util.StringUtils;

import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleResults;

/**
 * Generate a report based on CheckstyleResults.
 *
 * @version $Id$
 */
public class CheckstyleReportGenerator {
    private Log log;

    private final File basedir;

    private final ResourceBundle bundle;

    private final Sink sink;

    private SeverityLevel severityLevel;

    private Configuration checkstyleConfig;

    private boolean enableRulesSummary;

    private boolean enableSeveritySummary;

    private boolean enableFilesSummary;

    private boolean enableRSS;

    private final SiteTool siteTool;

    private String xrefLocation;

    private List<String> treeWalkerNames = Collections.singletonList("TreeWalker");

    private final IconTool iconTool;

    private final String ruleset;

    public CheckstyleReportGenerator(Sink sink, ResourceBundle bundle, File basedir, SiteTool siteTool,
                                     String ruleset) {
        this.bundle = bundle;

        this.sink = sink;

        this.basedir = basedir;

        this.siteTool = siteTool;

        this.ruleset = ruleset;

        this.enableRulesSummary = true;
        this.enableSeveritySummary = true;
        this.enableFilesSummary = true;
        this.enableRSS = true;
        this.iconTool = new IconTool(sink, bundle);
    }

    public Log getLog() {
        if (this.log == null) {
            this.log = new SystemStreamLog();
        }
        return this.log;
    }

    public void setLog(Log log) {
        this.log = log;
    }

    private String getTitle() {
        String title;

        if (getSeverityLevel() == null) {
            title = bundle.getString("report.checkstyle.title");
        } else {
            title = bundle.getString("report.checkstyle.severity_title") + severityLevel.getName();
        }

        return title;
    }

    public void generateReport(CheckstyleResults results) {
        doHeading();

        if (getSeverityLevel() == null) {
            if (enableSeveritySummary) {
                doSeveritySummary(results);
            }

            if (enableFilesSummary) {
                doFilesSummary(results);
            }

            if (enableRulesSummary) {
                doRulesSummary(results);
            }
        }

        doDetails(results);
        sink.body_();
        sink.flush();
        sink.close();
    }

    private void doHeading() {
        sink.head();
        sink.title();
        sink.text(getTitle());
        sink.title_();
        sink.head_();

        sink.body();

        sink.section1();
        sink.sectionTitle1();
        sink.text(getTitle());
        sink.sectionTitle1_();

        sink.paragraph();
        sink.text(bundle.getString("report.checkstyle.checkstylelink") + " ");
        sink.link("http://checkstyle.sourceforge.net/");
        sink.text("Checkstyle");
        sink.link_();
        String version = getCheckstyleVersion();
        if (version != null) {
            sink.text(" ");
            sink.text(version);
        }
        sink.text(" ");
        sink.text(String.format(bundle.getString("report.checkstyle.ruleset"), ruleset));
        sink.text(".");

        if (enableRSS) {
            sink.nonBreakingSpace();
            sink.link("checkstyle.rss");
            sink.figure();
            sink.figureCaption();
            sink.text("rss feed");
            sink.figureCaption_();
            sink.figureGraphics("images/rss.png");
            sink.figure_();
            sink.link_();
        }

        sink.paragraph_();
        sink.section1_();
    }

    /**
     * Get the value of the specified attribute from the Checkstyle configuration.
     * If parentConfigurations is non-null and non-empty, the parent
     * configurations are searched if the attribute cannot be found in the
     * current configuration. If the attribute is still not found, the
     * specified default value will be returned.
     *
     * @param config              The current Checkstyle configuration
     * @param parentConfiguration The configuration of the parent of the current configuration
     * @param attributeName       The name of the attribute
     * @param defaultValue        The default value to use if the attribute cannot be found in any configuration
     * @return The value of the specified attribute
     */
    private String getConfigAttribute(Configuration config, ChainedItem<Configuration> parentConfiguration,
                                      String attributeName, String defaultValue) {
        String ret;
        try {
            ret = config.getAttribute(attributeName);
        } catch (CheckstyleException e) {
            // Try to find the attribute in a parent, if there are any
            if (parentConfiguration != null) {
                ret =
                        getConfigAttribute(parentConfiguration.value, parentConfiguration.parent, attributeName,
                                defaultValue);
            } else {
                ret = defaultValue;
            }
        }
        return ret;
    }

    /**
     * Create the rules summary section of the report.
     *
     * @param results The results to summarize
     */
    private void doRulesSummary(CheckstyleResults results) {
        if (checkstyleConfig == null) {
            return;
        }

        sink.section1();
        sink.sectionTitle1();
        sink.text(bundle.getString("report.checkstyle.rules"));
        sink.sectionTitle1_();

        sink.table();

        sink.tableRow();
        sink.tableHeaderCell();
        sink.text(bundle.getString("report.checkstyle.rule.category"));
        sink.tableHeaderCell_();

        sink.tableHeaderCell();
        sink.text(bundle.getString("report.checkstyle.rule"));
        sink.tableHeaderCell_();

        sink.tableHeaderCell();
        sink.text(bundle.getString("report.checkstyle.violations"));
        sink.tableHeaderCell_();

        sink.tableHeaderCell();
        sink.text(bundle.getString("report.checkstyle.column.severity"));
        sink.tableHeaderCell_();

        sink.tableRow_();

        // Top level should be the checker.
        if ("checker".equalsIgnoreCase(checkstyleConfig.getName())) {
            String category = null;
            for (ConfReference ref : sortConfiguration(results)) {
                doRuleRow(ref, results, category);

                category = ref.category;
            }
        } else {
            sink.tableRow();
            sink.tableCell();
            sink.text(bundle.getString("report.checkstyle.norule"));
            sink.tableCell_();
            sink.tableRow_();
        }

        sink.table_();

        sink.section1_();
    }

    /**
     * Create a summary for one Checkstyle rule.
     *
     * @param ref              The configuration reference for the row
     * @param results          The results to summarize
     * @param previousCategory The previous row's category
     */
    private void doRuleRow(ConfReference ref, CheckstyleResults results, String previousCategory) {
        Configuration checkerConfig = ref.configuration;
        ChainedItem<Configuration> parentConfiguration = ref.parentConfiguration;
        String ruleName = checkerConfig.getName();

        sink.tableRow();

        // column 1: rule category
        sink.tableCell();
        String category = ref.category;
        if (!category.equals(previousCategory)) {
            sink.text(category);
        }
        sink.tableCell_();

        // column 2: Rule name + configured attributes
        sink.tableCell();
        if (!"extension".equals(category)) {
            sink.link("http://checkstyle.sourceforge.net/config_" + category + ".html#" + ruleName);
            sink.text(ruleName);
            sink.link_();
        } else {
            sink.text(ruleName);
        }

        List<String> attribnames = new ArrayList<>(Arrays.asList(checkerConfig.getAttributeNames()));
        attribnames.remove("severity"); // special value (deserves unique column)
        if (!attribnames.isEmpty()) {
            sink.list();
            for (String name : attribnames) {
                sink.listItem();

                sink.text(name);

                String value = getConfigAttribute(checkerConfig, null, name, "");
                // special case, Header.header and RegexpHeader.header
                if ("header".equals(name) && ("Header".equals(ruleName) || "RegexpHeader".equals(ruleName))) {
                    String[] lines = StringUtils.split(value, "\\n");
                    int linenum = 1;
                    for (String line : lines) {
                        sink.lineBreak();
                        sink.rawText("");
                        sink.text(linenum + ":");
                        sink.rawText("");
                        sink.nonBreakingSpace();
                        sink.monospaced();
                        sink.text(line);
                        sink.monospaced_();
                        linenum++;
                    }
                } else if ("headerFile".equals(name) && "RegexpHeader".equals(ruleName)) {
                    sink.text(": ");
                    sink.monospaced();
                    sink.text("\"");
                    if (basedir != null) {
                        // Make the headerFile value relative to ${basedir}
                        String path = siteTool.getRelativePath(value, basedir.getAbsolutePath());
                        sink.text(path.replace('\\', '/'));
                    } else {
                        sink.text(value);
                    }
                    sink.text("\"");
                    sink.monospaced_();
                } else {
                    sink.text(": ");
                    sink.monospaced();
                    sink.text("\"");
                    sink.text(value);
                    sink.text("\"");
                    sink.monospaced_();
                }
                sink.listItem_();
            }
            sink.list_();
        }

        sink.tableCell_();

        // column 3: rule violation count
        sink.tableCell();
        sink.text(String.valueOf(ref.violations));
        sink.tableCell_();

        // column 4: severity
        sink.tableCell();
        // Grab the severity from the rule configuration, this time use error as default value
        // Also pass along all parent configurations, so that we can try to find the severity there
        String severity = getConfigAttribute(checkerConfig, parentConfiguration, "severity", "error");
        iconTool.iconSeverity(severity, IconTool.TEXT_SIMPLE);
        sink.tableCell_();

        sink.tableRow_();
    }

    /**
     * Check if a violation matches a rule.
     *
     * @param event            the violation to check
     * @param ruleName         The name of the rule
     * @param expectedMessage  A message that, if it's not null, will be matched to the message from the violation
     * @param expectedSeverity A severity that, if it's not null, will be matched to the severity from the violation
     * @return The number of rule violations
     */
    public boolean matchRule(AuditEvent event, String ruleName, String expectedMessage, String expectedSeverity) {
        if (!ruleName.equals(RuleUtil.getName(event))) {
            return false;
        }

        // check message too, for those that have a specific one.
        // like GenericIllegalRegexp and Regexp
        if (expectedMessage != null) {
            // event.getMessage() uses java.text.MessageFormat in its implementation.
            // Read MessageFormat Javadoc about single quote:
            // http://java.sun.com/j2se/1.4.2/docs/api/java/text/MessageFormat.html
            String msgWithoutSingleQuote = StringUtils.replace(expectedMessage, "'", "");

            return expectedMessage.equals(event.getMessage()) || msgWithoutSingleQuote.equals(event.getMessage());
        }
        // Check the severity. This helps to distinguish between
        // different configurations for the same rule, where each
        // configuration has a different severity, like JavadocMetod.
        // See also http://jira.codehaus.org/browse/MCHECKSTYLE-41
        if (expectedSeverity != null) {
            return expectedSeverity.equals(event.getSeverityLevel().getName());
        }

        return true;
    }

    private void doSeveritySummary(CheckstyleResults results) {
        sink.section1();
        sink.sectionTitle1();
        sink.text(bundle.getString("report.checkstyle.summary"));
        sink.sectionTitle1_();

        sink.table();

        sink.tableRow();
        sink.tableHeaderCell();
        sink.text(bundle.getString("report.checkstyle.files"));
        sink.tableHeaderCell_();

        sink.tableHeaderCell();
        iconTool.iconInfo(IconTool.TEXT_TITLE);
        sink.tableHeaderCell_();

        sink.tableHeaderCell();
        iconTool.iconWarning(IconTool.TEXT_TITLE);
        sink.tableHeaderCell_();

        sink.tableHeaderCell();
        iconTool.iconError(IconTool.TEXT_TITLE);
        sink.tableHeaderCell_();
        sink.tableRow_();

        sink.tableRow();
        sink.tableCell();
        sink.text(String.valueOf(results.getFileCount()));
        sink.tableCell_();
        sink.tableCell();
        sink.text(String.valueOf(results.getSeverityCount(SeverityLevel.INFO)));
        sink.tableCell_();
        sink.tableCell();
        sink.text(String.valueOf(results.getSeverityCount(SeverityLevel.WARNING)));
        sink.tableCell_();
        sink.tableCell();
        sink.text(String.valueOf(results.getSeverityCount(SeverityLevel.ERROR)));
        sink.tableCell_();
        sink.tableRow_();

        sink.table_();

        sink.section1_();
    }

    private void doFilesSummary(CheckstyleResults results) {
        sink.section1();
        sink.sectionTitle1();
        sink.text(bundle.getString("report.checkstyle.files"));
        sink.sectionTitle1_();

        sink.table();

        sink.tableRow();
        sink.tableHeaderCell();
        sink.text(bundle.getString("report.checkstyle.file"));
        sink.tableHeaderCell_();
        sink.tableHeaderCell();
        iconTool.iconInfo(IconTool.TEXT_ABBREV);
        sink.tableHeaderCell_();
        sink.tableHeaderCell();
        iconTool.iconWarning(IconTool.TEXT_ABBREV);
        sink.tableHeaderCell_();
        sink.tableHeaderCell();
        iconTool.iconError(IconTool.TEXT_ABBREV);
        sink.tableHeaderCell_();
        sink.tableRow_();

        // Sort the files before writing them to the report
        List<String> fileList = new ArrayList<>(results.getFiles().keySet());
        Collections.sort(fileList);

        for (String filename : fileList) {
            List<AuditEvent> violations = results.getFileViolations(filename);
            if (violations.isEmpty()) {
                // skip files without violations
                continue;
            }

            sink.tableRow();

            sink.tableCell();
            sink.link("#" + filename.replace('/', '.'));
            sink.text(filename);
            sink.link_();
            sink.tableCell_();

            sink.tableCell();
            sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.INFO)));
            sink.tableCell_();

            sink.tableCell();
            sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.WARNING)));
            sink.tableCell_();

            sink.tableCell();
            sink.text(String.valueOf(results.getSeverityCount(violations, SeverityLevel.ERROR)));
            sink.tableCell_();

            sink.tableRow_();
        }

        sink.table_();
        sink.section1_();
    }

    private void doDetails(CheckstyleResults results) {

        sink.section1();
        sink.sectionTitle1();
        sink.text(bundle.getString("report.checkstyle.details"));
        sink.sectionTitle1_();

        // Sort the files before writing their details to the report
        List<String> fileList = new ArrayList<>(results.getFiles().keySet());
        Collections.sort(fileList);

        for (String file : fileList) {
            List<AuditEvent> violations = results.getFileViolations(file);

            if (violations.isEmpty()) {
                // skip files without violations
                continue;
            }

            sink.section2();
            SinkEventAttributes attrs = new SinkEventAttributeSet();
            attrs.addAttribute(SinkEventAttributes.ID, file.replace('/', '.'));
            sink.sectionTitle(Sink.SECTION_LEVEL_2, attrs);
            sink.text(file);
            sink.sectionTitle_(Sink.SECTION_LEVEL_2);

            sink.table();
            sink.tableRow();
            sink.tableHeaderCell();
            sink.text(bundle.getString("report.checkstyle.column.severity"));
            sink.tableHeaderCell_();
            sink.tableHeaderCell();
            sink.text(bundle.getString("report.checkstyle.rule.category"));
            sink.tableHeaderCell_();
            sink.tableHeaderCell();
            sink.text(bundle.getString("report.checkstyle.rule"));
            sink.tableHeaderCell_();
            sink.tableHeaderCell();
            sink.text(bundle.getString("report.checkstyle.column.message"));
            sink.tableHeaderCell_();
            sink.tableHeaderCell();
            sink.text(bundle.getString("report.checkstyle.column.line"));
            sink.tableHeaderCell_();
            sink.tableRow_();

            doFileEvents(violations, file);

            sink.table_();
            sink.section2_();
        }

        sink.section1_();
    }

    private void doFileEvents(List<AuditEvent> eventList, String filename) {
        for (AuditEvent event : eventList) {
            SeverityLevel level = event.getSeverityLevel();

            if ((getSeverityLevel() != null) && !(getSeverityLevel() != level)) {
                continue;
            }

            sink.tableRow();

            sink.tableCell();
            iconTool.iconSeverity(level.getName(), IconTool.TEXT_SIMPLE);
            sink.tableCell_();

            sink.tableCell();
            String category = RuleUtil.getCategory(event);
            if (category != null) {
                sink.text(category);
            }
            sink.tableCell_();

            sink.tableCell();
            String ruleName = RuleUtil.getName(event);
            if (ruleName != null) {
                sink.text(ruleName);
            }
            sink.tableCell_();

            sink.tableCell();
            sink.text(event.getMessage());
            sink.tableCell_();

            sink.tableCell();

            int line = event.getLine();
            if (getXrefLocation() != null && line != 0) {
                sink.link(getXrefLocation() + "/" + filename.replaceAll("\\.java$", ".html") + "#L"
                        + line);
                sink.text(String.valueOf(line));
                sink.link_();
            } else if (line != 0) {
                sink.text(String.valueOf(line));
            }
            sink.tableCell_();

            sink.tableRow_();
        }
    }

    public SeverityLevel getSeverityLevel() {
        return severityLevel;
    }

    public void setSeverityLevel(SeverityLevel severityLevel) {
        this.severityLevel = severityLevel;
    }

    public boolean isEnableRulesSummary() {
        return enableRulesSummary;
    }

    public void setEnableRulesSummary(boolean enableRulesSummary) {
        this.enableRulesSummary = enableRulesSummary;
    }

    public boolean isEnableSeveritySummary() {
        return enableSeveritySummary;
    }

    public void setEnableSeveritySummary(boolean enableSeveritySummary) {
        this.enableSeveritySummary = enableSeveritySummary;
    }

    public boolean isEnableFilesSummary() {
        return enableFilesSummary;
    }

    public void setEnableFilesSummary(boolean enableFilesSummary) {
        this.enableFilesSummary = enableFilesSummary;
    }

    public boolean isEnableRSS() {
        return enableRSS;
    }

    public void setEnableRSS(boolean enableRSS) {
        this.enableRSS = enableRSS;
    }

    public String getXrefLocation() {
        return xrefLocation;
    }

    public void setXrefLocation(String xrefLocation) {
        this.xrefLocation = xrefLocation;
    }

    public Configuration getCheckstyleConfig() {
        return checkstyleConfig;
    }

    public void setCheckstyleConfig(Configuration config) {
        this.checkstyleConfig = config;
    }

    public void setTreeWalkerNames(List<String> treeWalkerNames) {
        this.treeWalkerNames = treeWalkerNames;
    }

    public List<String> getTreeWalkerNames() {
        return treeWalkerNames;
    }

    /**
     * Get the effective Checkstyle version at runtime.
     *
     * @return the MANIFEST implementation version of Checkstyle API package (can be null)
     */
    private String getCheckstyleVersion() {
        Package checkstyleApiPackage = Configuration.class.getPackage();

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

    public List<ConfReference> sortConfiguration(CheckstyleResults results) {
        List<ConfReference> result = new ArrayList<>();

        sortConfiguration(result, checkstyleConfig, null, results);

        Collections.sort(result);

        return result;
    }

    private void sortConfiguration(List<ConfReference> result, Configuration config,
                                   ChainedItem<Configuration> parent, CheckstyleResults results) {
        for (Configuration childConfig : config.getChildren()) {
            String ruleName = childConfig.getName();

            if (treeWalkerNames.contains(ruleName)) {
                // special sub-case: TreeWalker is the parent of multiple rules, not an effective rule
                sortConfiguration(result, childConfig, new ChainedItem<>(config, parent), results);
            } else {
                String fixedmessage = getConfigAttribute(childConfig, null, "message", null);
                // Grab the severity from the rule configuration, use null as default value
                String configSeverity = getConfigAttribute(childConfig, null, "severity", null);

                // count rule violations
                long violations = 0;
                AuditEvent lastMatchedEvent = null;
                for (List<AuditEvent> errors : results.getFiles().values()) {
                    for (AuditEvent event : errors) {
                        if (matchRule(event, ruleName, fixedmessage, configSeverity)) {
                            lastMatchedEvent = event;
                            violations++;
                        }
                    }
                }

                if (violations > 0) // forget rules without violations
                {
                    String category = RuleUtil.getCategory(lastMatchedEvent);

                    result.add(new ConfReference(category, childConfig, parent, violations, result.size()));
                }
            }
        }
    }

    private static class ConfReference
            implements Comparable<ConfReference> {
        private final String category;
        private final Configuration configuration;
        private final ChainedItem<Configuration> parentConfiguration;
        private final long violations;
        private final int count;

        public ConfReference(String category, Configuration configuration,
                             ChainedItem<Configuration> parentConfiguration, long violations, int count) {
            this.category = category;
            this.configuration = configuration;
            this.parentConfiguration = parentConfiguration;
            this.violations = violations;
            this.count = count;
        }

        public int compareTo(ConfReference o) {
            int compare = category.compareTo(o.category);
            if (compare == 0) {
                compare = configuration.getName().compareTo(o.configuration.getName());
            }
            return (compare == 0) ? (o.count - count) : compare;
        }
    }

    private static class ChainedItem<T> {
        private final ChainedItem<T> parent;

        private final T value;

        public ChainedItem(T value, ChainedItem<T> parent) {
            this.parent = parent;
            this.value = value;
        }
    }

}


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

/**
 * @author igolovach
 */

public class CodeGenUtils {
    public static String TAB = "    ";

    public static String tab(String str) {
        return tab(1, str);
    }

    public static String tab(int tabCount, String str) {
        String[] arr = str.split("\n");
        StringBuilder result = new StringBuilder();
        for (String s : arr) {
            for (int i = 1; i <= tabCount; i++) {
                result.append(TAB);
            }
            result.append(s).append("\n");
        }
        if (!str.endsWith("\n")) {
            return result.substring(0, result.length() - 1);
        }
        return result.toString();
    }

    public static String firstLetterToUpperCase(String fieldName) {
        return fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
    }

    public static String firstLetterToLowerCase(String fieldName) {
        return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
    }

    /**
     * <p> Integer[][].class -> "Integer[][]"
     * <p> int[][].class -> "int[][]"
     * <p> Naive: Integer[][].class.getName() -> "[[Ljava.lang.Integer;"
     * <p> Naive: int[][].class.getName() -> "[[LI;"
     */
    public static String className(Class<?> clazz) { //todo: rename
        //todo: is that correct algorithm?
        final String naiveName = clazz.getName();
        if (!clazz.isArray()) { //todo: what about enum
            return naiveName;
        } else {
            int count = 0;
            String ending = "";
            while (clazz.isArray()) {
                count++;
                ending += "[]";
                clazz = clazz.getComponentType();
            }
            if (clazz.isPrimitive()) {
                String primitiveClassName;
                if (clazz == boolean.class) {
                    primitiveClassName = "boolean";
                } else if (clazz == byte.class) {
                    primitiveClassName = "byte";
                } else if (clazz == char.class) {
                    primitiveClassName = "char";
                } else if (clazz == short.class) {
                    primitiveClassName = "short";
                } else if (clazz == int.class) {
                    primitiveClassName = "int";
                } else if (clazz == long.class) {
                    primitiveClassName = "long";
                } else if (clazz == float.class) {
                    primitiveClassName = "float";
                } else if (clazz == double.class) {
                    primitiveClassName = "double";
                } else {
                    throw new RuntimeException("Never here! - You try to generate code for Void[]...[]: clazz = " + clazz);
                }
                return primitiveClassName + ending;
            } else {
                return naiveName.substring(count + 1, naiveName.length() - 1) + ending;
            }
        }
    }
}


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

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.client.ClientCache;
import com.gemstone.gemfire.cache.client.ClientCacheFactory;
import com.gemstone.gemfire.cache.client.ClientRegionFactory;
import com.gemstone.gemfire.cache.client.ClientRegionShortcut;
import com.googlecode.icegem.cacheutils.common.Utils;
import com.googlecode.icegem.utils.PropertiesHelper;

/**
 * The client cache which is used for replication test. Connects to the cluster
 * represented by a locator, puts name of the locator as a key and current time
 * as a value to the technical region and waits events from other GuestNodes.
 * After that calculates time of replication ant prints it to the system output.
 *
 * Returns 0 as exit code in case of all the expected events received, 1
 * otherwise.
 *
 */
public class GuestNode {
	/* Prefix of the gemfire system properties */
	private static final String GEMFIRE_PREFIX = "gemfire.";

	/* Polling period, ms */
	private static final int CHECK_PERIOD = 50;

	/* Prefix of the check replication tool keys */
	private static final String KEY_PREFIX = "check-replication-";

	/* Postfix of the check replication tool keys used for the startedAt entries */
	private static final String KEY_POSTFIX_STARTED_AT = "-startedAt";
	/* Postfix of the check replication tool keys used for the sentAt entries */
	private static final String KEY_POSTFIX_SENT_AT = "-sentAt";
	/*
	 * Postfix of the check replication tool keys used for the receivedAt
	 * entries
	 */
	private static final String KEY_POSTFIX_DURATION = "-receivedAt";

	/* Local cluster name */
	private String localClusterName;

	/* Cache instance */
	private ClientCache clientCache;

	/* Technical region instance */
	private Region<String, Long> region;

	/* The name of technical region */
	private String regionName;

	/* Clusters' properties */
	private Properties clustersProperties;

	/* Debug enabled flag */
	private boolean debugEnabled;

	/* Quiet flag */
	private boolean quiet;

	/* The time at which the processing has started */
	private long processingStartedAt;

	/**
	 * Creates the instance of guest node
	 *
	 * @param cluster
	 *            - the name of local cluster
	 * @param clustersProperties
	 *            - the clusters' properties
	 * @param regionName
	 *            - the name of the technical region
	 * @param debugEnabled
	 *            - the debug enabled flag
	 * @param quiet
	 *            - the quiet flag
	 * @param processingStartedAt
	 *            - the time at which the processing has started
	 */
	private GuestNode(String cluster, Properties clustersProperties,
		String regionName, boolean debugEnabled, boolean quiet,
		long processingStartedAt) {

		debug("GuestNode#GuestNode(String, Properties, String, String, String): Creating instance with parameters: cluster = "
			+ cluster
			+ ", clustersProperties = "
			+ clustersProperties
			+ ", regionName = " + regionName);

		this.localClusterName = cluster;
		this.clustersProperties = clustersProperties;
		this.regionName = regionName;
		this.debugEnabled = debugEnabled;
		this.quiet = quiet;
		this.processingStartedAt = processingStartedAt;

		debug("GuestNode#GuestNode(String, Properties, String, String, String): Creating RelationsController");

		init();
	}

	/**
	 * Creates the startedAt key
	 *
	 * @param clusterName
	 *            - the name of cluster
	 * @return - the startedAt key
	 */
	private String createStartedAtKey(String clusterName) {
		return KEY_PREFIX + clusterName + KEY_POSTFIX_STARTED_AT;
	}

	/**
	 * Creates the sentAt key
	 *
	 * @param clusterName
	 *            - the name of cluster
	 * @return - the sentAt key
	 */
	private String createSentAtKey(String clusterName) {
		return KEY_PREFIX + clusterName + KEY_POSTFIX_SENT_AT;
	}

	/**
	 * Creates the receivedAt key
	 *
	 * @param fromClusterName
	 *            - the name of cluster from which the entry received
	 * @param toClusterName
	 *            - the name of cluster on which the entry received
	 * @return - the receivedAt key
	 */
	private String createReceivedAtKey(String fromClusterName,
		String toClusterName) {

		return KEY_PREFIX + fromClusterName + "-" + toClusterName
			+ KEY_POSTFIX_DURATION;
	}

	/**
	 * Initializes the technical region
	 */
	private void init() {
		try {
			debug("GuestNode#init(): Creating Cache");

			ClientCacheFactory clientCacheFactory = new ClientCacheFactory();

			Properties gemfireProperties = PropertiesHelper.filterProperties(
				System.getProperties(), GEMFIRE_PREFIX);

			for (Object keyObject : gemfireProperties.keySet()) {
				String key = (String) keyObject;
				String value = gemfireProperties.getProperty(key);

				String name = key.substring(GEMFIRE_PREFIX.length());

				debug("GuestNode#init(): Configuring ClientCacheFactory with key = "
					+ name + ", value = " + value);

				clientCacheFactory.set(name, value);
			}

			clientCacheFactory.setPoolSubscriptionEnabled(true);

			String locators = clustersProperties.getProperty(localClusterName);
			String[] locatorsArray = locators.split(",");
			for (String locator : locatorsArray) {
				String locatorHost = locator.substring(0, locator.indexOf("["));

				String locatorPortString = locator.substring(
					locator.indexOf("[") + 1, locator.indexOf("]"));
				int locatorPort = Integer.parseInt(locatorPortString);

				debug("GuestNode#init(): Adding locator to pool: locatorHost = "
					+ locatorHost + ", locatorPort = " + locatorPort);

				clientCacheFactory.addPoolLocator(locatorHost, locatorPort);
			}

			clientCache = clientCacheFactory.create();

			ClientRegionFactory<String, Long> clientRegionFactory = clientCache
				.createClientRegionFactory(ClientRegionShortcut.PROXY);

			region = clientCache.getRegion(regionName);

			debug("GuestNode#init(): Get region with name = " + regionName
				+ ": region = " + region);

			if (region == null) {
				region = clientRegionFactory.create(regionName);
			}
			debug("GuestNode#init(): Create region with name = " + regionName
				+ ": region = " + region);

		} catch (Throwable t) {
			debug(
				"GuestNode#init(): Throwable caught with message = "
					+ t.getMessage(), t);

		}
	}

	/**
	 * Wait until the other clients started
	 */
	private void waitForStarted() {
		debug("GuestNode#waitForStarted(): Waiting for other clusters started");

		while (true) {
			boolean othersStarted = true;

			for (Object key : clustersProperties.keySet()) {
				String clusterName = (String) key;

				Long startedAt = region.get(createStartedAtKey(clusterName));

				debug("GuestNode#waitForStarted(): Checking startedAt: startedAt = "
					+ startedAt
					+ ", processingStartedAt = "
					+ processingStartedAt);

				if ((startedAt == null)
					|| (startedAt.longValue() < processingStartedAt)) {

					othersStarted = false;
					break;

				}
			}

			if (othersStarted) {
				break;
			}

			try {
				TimeUnit.MILLISECONDS.sleep(CHECK_PERIOD);
			} catch (InterruptedException e) {
			}
		}

		debug("GuestNode#waitForStarted(): Other clusters started");
	}

	/**
	 * Wait until received all the entries from other clients. Put the
	 * receivedAt entries to cache.
	 */
	private void waitForSent() {
		debug("GuestNode#waitForSent(): Waiting for other clusters sent");

		while (true) {
			boolean othersSent = true;

			Map<String, Long> clusterNameToReceivedAtMap = new HashMap<String, Long>();
			for (Object key : clustersProperties.keySet()) {
				String clusterName = (String) key;

				if (localClusterName.equals(clusterName)) {
					continue;
				}

				Long sentAt = region.get(createSentAtKey(clusterName));
				long receivedAt = System.currentTimeMillis();

				if ((sentAt != null)
					&& (sentAt.longValue() > processingStartedAt)) {

					clusterNameToReceivedAtMap.put(clusterName, receivedAt);
				}
			}

			for (Object key : clustersProperties.keySet()) {
				String clusterName = (String) key;

				if (localClusterName.equals(clusterName)) {
					continue;
				}

				Long receivedAt = clusterNameToReceivedAtMap.get(clusterName);

				if (receivedAt == null) {

					if (othersSent) {
						othersSent = false;
					}

				} else {
					region.put(
						createReceivedAtKey(clusterName, localClusterName),
						receivedAt);
				}
			}

			if (othersSent) {
				break;
			}

			try {
				TimeUnit.MILLISECONDS.sleep(CHECK_PERIOD);
			} catch (InterruptedException e) {
			}
		}

		debug("GuestNode#waitForSent(): Other clusters sent");
	}

	/**
	 * Wait until all the clients received all the receivedAt entries.
	 */
	private void waitForConnected() {
		debug("GuestNode#waitForConnected(): Waiting for all the clusters connected");

		while (true) {
			boolean connected = true;

			for (Object fromKey : clustersProperties.keySet()) {
				String fromClusterName = (String) fromKey;

				for (Object toKey : clustersProperties.keySet()) {
					String toClusterName = (String) toKey;

					if (fromClusterName.equals(toClusterName)) {
						continue;
					}

					Long receivedAt = region.get(createReceivedAtKey(
						fromClusterName, toClusterName));

					if (receivedAt == null) {
						connected = false;
						break;
					}
				}
			}

			if (connected) {
				break;
			}

			try {
				TimeUnit.MILLISECONDS.sleep(CHECK_PERIOD);
			} catch (InterruptedException e) {
			}
		}

		debug("GuestNode#waitForConnected(): All the clusters connected");
	}

	/**
	 * The processing task. Contains the main algorithm of the GuestNode:
	 *

	 *
Put startedAt entry with a special startedAt key and date of the
	 * start as the value

	 *
Wait until in the local region will be all the startedAt entries from
	 * other clients

	 *
Put sentAt entry with a special sentAt key and date of the start as
	 * the value

	 *
Wait until in the local region will be all the sentAt entries from
	 * other clients. When such entry received, put new special entry receivedAt
	 * into the region

	 *
Wait until in the local region will be all the receivedAt entries
	 * from other clients

	 *

	 *
	 * See the special keys formats in the appropriate methods.
	 */
	private class ProcessingTask implements Runnable {
		private boolean connected = false;

		public void run() {
			try {
				region.put(createStartedAtKey(localClusterName),
					System.currentTimeMillis());

				waitForStarted();

				region.put(createSentAtKey(localClusterName),
					System.currentTimeMillis());

				waitForSent();

				waitForConnected();

				connected = true;
			} catch (Throwable t) {
				connected = false;
			}
		}

		public boolean isConnected() {
			return connected;
		}
	}

	/**
	 * Waits for processing finished.
	 *
	 * @param timeout
	 *            - the timeout in milliseconds
	 * @return - true if the connection process finished successfully, false
	 *         otherwise
	 */
	public boolean waitFor(long timeout) {
		debug("GuestNode#waitFor(long): Waiting for task finish with timeout = "
			+ timeout);

		ProcessingTask connectionCheckTask = new ProcessingTask();

		Utils.execute(connectionCheckTask, timeout);

		boolean connected = connectionCheckTask.isConnected();

		debug("GuestNode#waitFor(long): Task finished connected = " + connected);

		return connected;
	}

	/**
	 * Finalizes work with the guest node
	 */
	public void close() {
		try {
			debug("GuestNode#close(): Closing the cache");

			clientCache.close();

			debug("GuestNode#close(): Cache closed = " + clientCache.isClosed());
		} catch (Throwable t) {
			debug(
				"GuestNode#close(): Throwable caught with message = "
					+ t.getMessage(), t);
		}
	}

	/**
	 * Prints the current state of connections
	 *
	 * @param connected
	 */
	public void printState(boolean connected) {
		if (!quiet) {

			StringBuilder sb = new StringBuilder();

			if (connected) {

				sb.append(localClusterName).append(" <= ");

				Iterator<Object> it = clustersProperties.keySet().iterator();
				while (it.hasNext()) {
					String clusterName = (String) it.next();

					if (localClusterName.equals(clusterName)) {
						continue;
					}

					Long sentAt = region.get(createSentAtKey(clusterName));
					Long receivedAt = region.get(createReceivedAtKey(
						clusterName, localClusterName));

					long duration = receivedAt - sentAt;

					sb.append("[").append(clusterName).append(", ")
						.append(duration).append("ms]");
				}

			} else {

				sb.append("Connection process is not finished for ").append(
					localClusterName);

			}

			System.out.println(sb.toString());

		}
	}

	/**
	 * Configures and starts the guest node
	 *
	 * @param args
	 *            - the configuration arguments
	 */
	public static void main(String[] args) {
		try {
			if (args.length != 7) {
				Utils.exitWithFailure();
			}

			String cluster = args[0];
			Properties clustersProperties = PropertiesHelper
				.stringToProperties(args[1]);
			long timeout = Long.parseLong(args[2]);
			String regionName = args[3];
			boolean debugEnabled = ("true".equals(args[4]) ? true : false);
			boolean quiet = ("true".equals(args[5]) ? true : false);
			long processingStartedAt = Long.parseLong(args[6]);

			GuestNode guestNode = new GuestNode(cluster, clustersProperties,
				regionName, debugEnabled, quiet, processingStartedAt);

			boolean connected = guestNode.waitFor(timeout);

			guestNode.printState(connected);

			guestNode.close();

			if (connected) {
				Utils.exitWithSuccess();
			}

			Utils.exitWithFailure();
		} catch (Throwable t) {
			Utils.exitWithFailure();
		}
	}

	/**
	 * Prints debug information if the debug is enabled
	 *
	 * @param message
	 *            - the debug message
	 */
	private void debug(String message) {
		debug(message, null);
	}

	/**
	 * Prints debug information if the debug is enabled
	 *
	 * @param message
	 *            - the debug message
	 * @param t
	 *            - the instance of Throwable
	 */
	private void debug(String message, Throwable t) {
		if (debugEnabled) {
			long currentTime = System.currentTimeMillis();
			long timeSinceProcessingStart = currentTime - processingStartedAt;
			System.err.println(timeSinceProcessingStart + " ["
				+ localClusterName + "] " + message);

			if (t != null) {
				t.printStackTrace(System.err);
			}
		}
	}
}


/*-
 * #%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.attribute.AbstractAttributes;
import se.jguru.nazgul.tools.visualization.model.diagram.attribute.DotProperty;
import se.jguru.nazgul.tools.visualization.model.diagram.attribute.types.ArrowType;
import se.jguru.nazgul.tools.visualization.model.diagram.attribute.types.PointOrRectangle;
import se.jguru.nazgul.tools.visualization.model.diagram.attribute.types.StandardCssColor;
import se.jguru.nazgul.tools.visualization.model.diagram.attribute.types.TokenValueHolder;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 *
Attribute renderer which sorts its key/value pairs alphabetically, corresponding to
 * the DOT grammar specification below. This implementation only uses comma for
 * delimiters ({@link #DELIMITER}.

 *

 *     attr_list : '[' [ a_list ] ']' [ attr_list ]
 *     a_list    : ID '=' ID [ (';' | ',') ] [ a_list ]
 *

 *
Typically renders into something like the following:

 *

 *     [ sides="7", distortion="-0.687574", orientation="58", skew="-0.180116", color="lightsteelblue1" ] ;
 *

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

    /**
     * The token starting an AttributeList.
     */
    public static final String START_TOKEN = "[ ";

    /**
     * The token ending an AttributeList.
     */
    public static final String END_TOKEN = " ]";

    /**
     * The token separating attribute (key/value) pairs from one another.
     */
    public static final String DELIMITER = ", ";

    /**
     * The token separating key and value attributes from one another.
     */
    public static final String SEPARATOR = "=";

    /**
     * Default constructor.
     */
    public AttributeRenderer() {
        super(AbstractAttributes.class);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("PMD")
    protected String doRender(final RenderConfiguration config, final AbstractAttributes entity) {

        // Collect all non-null properties into a SortedMap using reflection.
        final SortedMap<String, String> dotAttributes = new TreeMap<>();

        for (Class<?> current = entity.getClass();
             current != null && current != Object.class;
             current = current.getSuperclass()) {

            for (Field currentField : current.getDeclaredFields()) {
                if (isConfigurationField(currentField)) {

                    String key = null;
                    Object value = null;

                    try {

                        // #1) Do we have a non-null value in the Field?
                        value = currentField.get(entity);
                        if (value == null) {
                            continue;
                        }

                        // #2) Do we have @DotProperty annotation on the Field?
                        final DotProperty dotPropertyAnnotation = currentField.getAnnotation(DotProperty.class);
                        if (dotPropertyAnnotation == null) {
                            continue;
                        }

                        // #3) Does this field need special treatment?
                        if (dotPropertyAnnotation.specialTreatment()) {

                            if ("labelSize".equals(currentField.getName())
                                    && PointOrRectangle.class.equals(currentField.getType())) {

                                final PointOrRectangle rect = (PointOrRectangle) value;
                                dotAttributes.put(quote("lwidth"),
                                        BigDecimal.valueOf(rect.getxOrWidth()).toPlainString());
                                dotAttributes.put(quote("lheight"),
                                        BigDecimal.valueOf(rect.getyOrHeight()).toPlainString());

                                // All Done for this Field.
                                continue;
                            }
                        }

                        // #4) Use the DotProperty "name" attribute, or fallback to the field name.
                        key = dotPropertyAnnotation.name();
                        if (key.isEmpty() || "##default".equalsIgnoreCase(key)) {
                            key = currentField.getName();
                        }

                        // #5) Transform the value if required, and add the key/value pair to the outbound Map.
                        dotAttributes.put(key, getDotConfigValueFor(value));

                    } catch (Exception e) {
                        throw new IllegalArgumentException("Could not add configuration for field ["
                                + currentField + "] --> [" + key + "] in class ["
                                + entity.getClass().getSimpleName() + "]", e);
                    }
                }
            }
        }

        // Check sanity
        if (dotAttributes.isEmpty()) {
            return "";
        }

        // Synthesize the required attribute List form.
        final StringBuilder builder = new StringBuilder(START_TOKEN);
        for (Map.Entry<String, String> current : dotAttributes.entrySet()) {
            builder.append(current.getKey() + SEPARATOR + "\"" + current.getValue() + "\"" + DELIMITER);
        }

        // Remove the last delimiter
        builder.delete(builder.length() - DELIMITER.length(), builder.length());

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

    //
    // Private helpers
    //

    private boolean isConfigurationField(final Field aField) {

        final int modifiers = aField.getModifiers();
        return Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers);
    }

    @SuppressWarnings("CheckStyle")
    private String getDotConfigValueFor(final Object modelValue) {

        // #1) If the modelValue corresponds to a specific type, render that type
        if (modelValue instanceof StandardCssColor) {
            return ((StandardCssColor) modelValue).getRgbValue();
        }
        if (modelValue instanceof TokenValueHolder) {
            return ((TokenValueHolder) modelValue).getTokenValue();
        }
        if (modelValue instanceof PointOrRectangle) {

            final PointOrRectangle toConvert = (PointOrRectangle) modelValue;
            final String xValue = BigDecimal.valueOf(toConvert.getxOrWidth()).toPlainString();
            final String yValue = BigDecimal.valueOf(toConvert.getyOrHeight()).toPlainString();

            return "(" + xValue + "," + yValue + ")" + (toConvert.isUnchangeable() ? "!" : "");
        }
        if(modelValue instanceof ArrowType) {

            final ArrowType arrowType = (ArrowType) modelValue;

            final String openModifier = arrowType.isFilled() ? "" : "o";
            final String clip = (arrowType.getClipSide() == null ? "" : arrowType.getClipSide().getTokenValue());

            // Done?
            return openModifier + clip + arrowType.getShape().getTokenValue();
        }

        // #2) If the modelValue is a String or primitive, simply use it.
        if (modelValue instanceof String || modelValue.getClass().isPrimitive()) {
            return "" + modelValue;
        }
        if (modelValue.getClass().getName().startsWith("java.lang.")) {
            return "" + modelValue;
        }

        // Unknown
        throw new IllegalArgumentException("Could not convert modelValue [" + modelValue
                + "] to a DOT configuration value.");
    }
}


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

import com.sun.javadoc.RootDoc;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Abstract facade to a graphviz executable program.
 *
 * @author Lennart Jörelid, jGuru Europe AB
 */
public abstract class AbstractExecutableFacade {

    /**
     * Regular expression to identify that an executable actually is Graphviz.
     */
    public static final String GRAPHVIZ_EXECUTABLE_FIRST_LINE_CHECK = "^.*[Gg][Rr][Aa][Pp][Hh][Vv][Ii][Zz].*$";

    /**
     * The System property key defining the graphviz home. If the result of
     * {@code System.getProperty(HOMEDIR_SYSTEM_PROPERTY)} returns a non-null value, it is assumed to be
     * the absolute path to the installation directory of Graphviz.
     */
    public static final String HOMEDIR_SYSTEM_PROPERTY = "graphviz.home";

    /**
     * The environment key defining the graphviz home. If the result of
     * {@code System.getenv(HOMEDIR_ENV_PROPERTY)} returns a non-null value, it is assumed to be
     * the absolute path to the installation directory of Graphviz.
     */
    public static final String HOMEDIR_ENV_PROPERTY = "GRAPHVIZ_HOME";

    /**
     * The surrounding operating system's name.
     */
    public static final String OS_NAME = System.getProperty("os.name");

    /**
     * Standard encoding for emitted files is UTF8.
     */
    public static final String UTF8_ENCODING = "UTF-8";

    // Internal state
    private List<String> discoveryLog;
    private String executableNameOrPath;
    private File homeDirectory;
    private boolean isInitializedOK;

    /**
     * Compound constructor creating an abstract facade wrapping the given executable.
     *
     * @param executableName The name of the executable to wrap, typically "dot.exe" or "dot" depending on the
     *                       underlying operating system.
     */
    protected AbstractExecutableFacade(final String executableName) {

        // Check sanity
        if (executableName == null || executableName.isEmpty()) {
            throw new IllegalArgumentException("Cannot handle null or empty 'executableName' argument.");
        }

        // Assign internal state
        this.discoveryLog = new ArrayList<>();

        // Find the Graphvis home directory
        try {

            // #1) Attempt to find the Graphviz directory from a System property
            String graphvizHome = System.getProperty(HOMEDIR_SYSTEM_PROPERTY);
            if (graphvizHome != null) {
                discoveryLog.add("GraphViz home [" + graphvizHome + "] found using the '"
                        + HOMEDIR_SYSTEM_PROPERTY + "' system property.");
            } else {

                discoveryLog.add("The '" + HOMEDIR_SYSTEM_PROPERTY + "' system property was not specified.");

                // #2) Attempt to find the Graphviz directory from an environment property
                graphvizHome = System.getenv(HOMEDIR_ENV_PROPERTY);
                if (graphvizHome != null) {
                    discoveryLog.add("GraphViz home [" + graphvizHome + "] found using the '"
                            + HOMEDIR_ENV_PROPERTY + "' environment variable.");
                } else {
                    discoveryLog.add("The '" + HOMEDIR_ENV_PROPERTY + "' environment variable was not specified.");
                }
            }

            // Check sanity
            if (graphvizHome != null) {

                homeDirectory = new File(graphvizHome);

                if (!homeDirectory.exists() || !homeDirectory.isDirectory()) {
                    discoveryLog.add("The specified GraphViz home directory does not exist: "
                            + homeDirectory.getPath());
                    homeDirectory = null;
                }
            }

            if (homeDirectory == null) {
                discoveryLog.add("System path will be used as GraphViz home directory was not specified.");
            }
        } catch (Exception e) {
            // Ignore this.
        }

        // Find the actual executable name, compensating for the operating system path eccentricities.
        if (OS_NAME != null && OS_NAME.contains("Windows")) {

            // Windows
            this.executableNameOrPath = (homeDirectory != null ? homeDirectory.getAbsolutePath() + File.separator : "")
                    + executableName + ".exe";
        } else {

            // Linux/Mac
            this.executableNameOrPath = executableName;
        }
    }

    /**
     *
This method should be invoked from within subclasses' worker methods, to validate that the executable found
     * is actually a GraphViz program.

     *
The method fires the command executableNameOrPath -V, capturing the output to determine
     * if it contains the pattern {@link #GRAPHVIZ_EXECUTABLE_FIRST_LINE_CHECK}, in which case it is deemed to be a
     * proper GraphViz executable. For instance, the two executables dot and neato responds as follows:

     *

     *
dot -V: dot - graphviz version 2.36.0 (20140111.2315)

     *
neato -V: neato - graphviz version 2.36.0 (20140111.2315)

     *

     *
     * @param rootDoc The non-null RootDoc received from the JavaDoc execution context.
     * @return {@link true} if this {@link AbstractExecutableFacade} is properly initialized and can accept commands.
     */
    protected boolean initialize(final RootDoc rootDoc) {

        // #0) Don't initialize twice, unless required to.
        if (isInitializedOK) {
            return true;
        }

        // #1) Output the discovery log.
        for (String current : discoveryLog) {
            rootDoc.printNotice(current);
        }

        rootDoc.printNotice("GraphViz home: " + getHomeDirectory().getAbsolutePath());
        rootDoc.printNotice("Executable   : " + getExecutableNameOrPath());

        // #2) Create a ProcessBuilder which synthesizes a call to the executable and determine its version.
        //
        final ProcessBuilder pb = new ProcessBuilder(getExecutableNameOrPath(), "-V");
        pb.redirectErrorStream(true);
        if (homeDirectory != null) {
            pb.directory(homeDirectory);
        }

        Process p;
        try {

            // Launch the executable
            p = pb.start();
        } catch (IOException e) {

            rootDoc.printWarning(e.getMessage());
            return false;
        }

        try (BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
             OutputStream out = p.getOutputStream();) {

            // We don't need to send any more commands to the process, so close the output stream.
            out.close();

            // Read all input from the process, searching for the pattern "graphviz" ignoring case.
            String line = null;
            while ((line = in.readLine()) != null) {

                if (line.matches(GRAPHVIZ_EXECUTABLE_FIRST_LINE_CHECK)) {

                    // We found the expected output from the executable.
                    rootDoc.printNotice("GraphViz Version: " + line);
                    isInitializedOK = true;
                    return true;

                } else {

                    // Unknown output. Printout a warning and die.
                    rootDoc.printWarning("Unknown GraphViz output: " + line);
                }
            }

            // Cache the result.
            isInitializedOK = false;
            return false;

        } catch (IOException e) {

            // Nopes.
            rootDoc.printWarning("Problem detecting GraphViz: " + e.getMessage());
            isInitializedOK = false;
            return false;

        } finally {

            for (; ; ) {
                try {
                    p.waitFor();
                    break;
                } catch (InterruptedException e) {
                    // Ignore this
                }
            }
        }
    }

    //
    // Private helpers
    //

    /**
     * @return The name or path to the Executable.
     */
    protected String getExecutableNameOrPath() {
        return executableNameOrPath;
    }

    /**
     * @return The File to the HomeDirectory.
     */
    protected File getHomeDirectory() {
        return homeDirectory;
    }
}


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

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

    /**
     * Default constructor.
     */
    public IdentifierRenderer() {
        super(Identifier.class);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String doRender(final RenderConfiguration config, final Identifier entity) {
        return config.getIndent() + quote(entity.getId()) + " = " + quote(entity.getTargetIdentifier());
    }
}


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

import com.sun.javadoc.RootDoc;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;

/**
 * Facade to the Dot executable from the GraphViz distribution.
 *
 * @author Lennart Jörelid, jGuru Europe AB
 */
public class DotFacade extends AbstractExecutableFacade {

    private static final String DOT_EXECUTABLE_NAME = "dot";

    /**
     * Default constructor, using "dot" as the e
     */
    public DotFacade() {
        super(DOT_EXECUTABLE_NAME);
    }

    /**
     *
Creates and writes PNG and HTML imagemap files by executing 'dot', with the following arguments:

     *

     *
     *         dot -Tcmapx -o [outputDir/filename].map -Tpng -o [outputDir/filename].png
     *
     *

     *
The {@code diagram} string is fed to the dot process.

     *
     * @param rootDoc         The active {@link RootDoc} instance.
     * @param dotDiagram      The diagram source to feed into 'dot' for rendering.
     * @param outputDirectory The directory where the result should be sent.
     * @param filename        The filename of the PNG and MAP files generated.
     * @throws IOException If the files could not be properly generated.
     */
    public void writePngImageAndImageMap(final RootDoc rootDoc,
                                         final String dotDiagram,
                                         final File outputDirectory,
                                         final String filename) throws IOException {

        // Check sanity
        if (filename == null || filename.isEmpty()) {
            throw new IllegalArgumentException("Cannot handle null or empty 'filename' argument.");
        }
        if (rootDoc == null) {
            throw new IllegalArgumentException("Cannot handle null 'rootDoc' argument.");
        }

        // #0) Initialize
        if (!initialize(rootDoc)) {
            rootDoc.printWarning("Could not initialize DotFacade. Aborting writing PNG image and ImageMap files.");
        }

        // #1) Ensure that the PNG and MAP files can be written to.
        final File pngFile = new File(outputDirectory, filename + ".png");
        final File mapFile = new File(outputDirectory, filename + ".map");
        pngFile.delete();
        mapFile.delete();

        // #2) Compile the arguments used to launch Graphviz.
        final ProcessBuilder pb = new ProcessBuilder(
                getExecutableNameOrPath(),
                "-Tcmapx", "-o", mapFile.getAbsolutePath(),
                "-Tpng", "-o", pngFile.getAbsolutePath());
        pb.redirectErrorStream(true);
        if (getHomeDirectory() != null) {
            pb.directory(getHomeDirectory());
        }

        // #3) Launch Graphviz. Harvest output.
        final Process p = pb.start();
        try (final BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
             final Writer out = new OutputStreamWriter(p.getOutputStream(), AbstractExecutableFacade.UTF8_ENCODING)) {

            // Send the diagram to the process, and close the stream after that.
            out.write(dotDiagram);
            out.close();

            // Got error output?
            String line = null;
            while ((line = in.readLine()) != null) {
                System.err.println(line);
            }

        } finally {

            while (true) {
                try {
                    int result = p.waitFor();
                    if (result != 0) {
                        throw new IllegalStateException("GraphViz exited with a non-zero return value: " + result);
                    }
                    break;
                } catch (InterruptedException e) {
                    // Ignore
                }
            }
        }
    }
}


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

import java.util.Collections;
import java.util.List;

import com.googlecode.icegem.serialization.AutoSerializable;
import com.googlecode.icegem.serialization.BeanVersion;

/**
 * Wrapper class for java.lang.Class + useful methods for code generation
 *
 * @author igolovach
 */

public class XClass {
    private final Class<?> clazz;
	private byte beanVersion;

    public XClass(Class<?> clazz) {
        this.clazz = clazz;

        BeanVersion annotation = getType().getAnnotation(BeanVersion.class);
        if (annotation != null) {
			beanVersion = annotation.value();
            // checks on positive value
            if (beanVersion < 1) {
                throw new RuntimeException("Value of annotation @BeanVersion must be positive, current value = " + beanVersion + " (class '" + clazz + "')");
            }
        } else {
            throw new RuntimeException("Class must be annotated with @BeanVersion: " + getType().getCanonicalName());
        }
    }

    public Class<?> getType() {
        return clazz;
    }

    /**
     * Returns properties in "natural" order, i.e. "ORDER BY VERSION, DECLARING CLASS, NAME"
     *
     * @return
     */
    public List<XProperty> getOrderedProperties() {
        final List<XProperty> result = Introspector.getProperties(clazz);
        Collections.sort(result, new XProperty.NaturalOrder());
        return result;
    }

    public byte getBeanVersion() {
        return beanVersion;
    }

    public byte getVersionHistoryLength() {
        AutoSerializable annotation = getType().getAnnotation(AutoSerializable.class);
		byte versionHistoryLength = annotation.versionHistoryLength();

        if (versionHistoryLength < 1) {
            throw new IllegalArgumentException("Version history length of annotation @AutoSerializable must be positive, current value = " + versionHistoryLength + " (class '" + clazz.getCanonicalName() + "')");
        }

        // At least the current version hash code is recorded to ensure proper match
        versionHistoryLength++;

        if(versionHistoryLength > getBeanVersion()) {
        	versionHistoryLength = getBeanVersion();
        }
        return versionHistoryLength;
    }

	public String getName() {
		return this.clazz.getName();
	}

    /**
     * Returns a hash code of class model for specified bean version based on fields that this model contains.
     *
     * @param version bean version to get metadata hashcode.
     * @return the hashcode (16bits)
     */
    public short getVersionModelHashCode(int version) {
    	List<XProperty> classFields = getOrderedProperties();

        StringBuilder builder = new StringBuilder();
        for (XProperty field : classFields) {
            if (version == -1 || version >= field.getPropertyVersion()) {
                builder.append(field.getType()).append(field.getName());
            }
        }
        int hashCode = builder.toString().hashCode();
		return (short) ((hashCode & 0xFFFF ) ^ ((hashCode & 0xFFFF0000) >> 16)) ;
    }

}


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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gemstone.bp.edu.emory.mathcs.backport.java.util.Collections;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.RegionService;
import com.gemstone.gemfire.cache.query.FunctionDomainException;
import com.gemstone.gemfire.cache.query.NameResolutionException;
import com.gemstone.gemfire.cache.query.Query;
import com.gemstone.gemfire.cache.query.QueryException;
import com.gemstone.gemfire.cache.query.QueryInvocationTargetException;
import com.gemstone.gemfire.cache.query.QueryService;
import com.gemstone.gemfire.cache.query.RegionNotFoundException;
import com.gemstone.gemfire.cache.query.SelectResults;
import com.gemstone.gemfire.cache.query.Struct;
import com.gemstone.gemfire.cache.query.TypeMismatchException;
import com.googlecode.icegem.utils.CacheUtils;

/**
 * This component allows to execute paginated queries both from client and
 * peer/server sides. It caches paginated query results in a help region and
 * allows to iterate on them using paginated query API.
 *
 * See http://code.google.com/p/icegem/wiki/Documentation#Paginated_Query for
 * more details and examples.
 *
 * RESTRICTIONS: 1). A query string for paginated query can be arbitrarily
 * complex but entry key must be part of projection list. 2). For partitioned
 * regions a query string must meet the requirements described in a GemFire
 * documentation for querying partitioned regions.
 *
 * ORDER BY on Partitioned Regions A paginated query supports order by
 * functionality on partitioned regions. The fields specified in the order by
 * clause must be part of the projection list.
 *
 * Limiting of results: Paginated query result can be limited. By default this
 * limit is 1000 entries. You can specify a custom limit value via paginated
 * query constructors argument. If query results exceeds this limit: - only a
 * specified limit number of entries will be cached and returned; - flag
 * 'limitExceeded' will be set to 'true'.
 *
 * Examples:
 *
 * SELECT * FROM /data.keySet; SELECT DISTINCT d.key, d.value.field1 FROM
 * /data.entrySet d ORDER BY d.value.field1;
 *
 * @author Andrey Stepanov aka standy
 */
public class PaginatedQuery<V> {
	/** page size by default */
	public static final int DEFAULT_PAGE_SIZE = 20;
	/** Default limit on query result */
	public static final int DEFAULT_QUERY_LIMIT = 1000;
	/**
	 * number of page that will be store general information about paginated
	 * query (e.g. total number of query entries)
	 */
	public static final int PAGE_NUMBER_FOR_GENERAL_INFO = -1;
	/** name of a help region for storing information about paginated queries */
	public static final String PAGINATED_QUERY_INFO_REGION_NAME = "paginated_query_info";
	/** Field logger */
	private static final Logger logger = LoggerFactory
			.getLogger(PaginatedQuery.class);

	/** Field queryService */
	private QueryService queryService;
	/** region for querying */
	private Region<Object, V> queryRegion;
	/** help region for storing information about paginated queries */
	private Region<PageKey, List<Object>> paginatedQueryInfoRegion;

	/** Field currentPageNumber */
	private int pageSize;
	/** Field queryString */
    private String queryString;
	/** Field queryParams */
    private Object[] queryParams;
	/** limit on query result */
	private int queryLimit;
    /** flag that indicates that information has been loaded */
	private boolean infoLoaded;
	/** flag that indicates that limit has been exceeded */
	private boolean limitExceeded;
	/** Field totalNumberOfEntries */
	private int totalNumberOfEntries;

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param queryLimit
	 *            limit on query result
	 * @param region
	 *            The region for querying.
	 * @param queryString
	 *            query string that must return entry keys
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService, int queryLimit,
			Region<Object, V> region, String queryString)
			throws RegionNotFoundException {
		this(queryService, queryLimit, region, queryString, DEFAULT_PAGE_SIZE);
	}

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param queryLimit
	 *            limit on query result
	 * @param region
	 *            The region for querying.
	 * @param queryString
	 *            query string that must return entry keys
	 * @param pageSize
	 *            size of page
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService, int queryLimit,
			Region<Object, V> region, String queryString, int pageSize)
			throws RegionNotFoundException {
		this(queryService, queryLimit, region, queryString, new Object[] {},
				pageSize);
	}

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param queryLimit
	 *            limit on query result
	 * @param region
	 *            The region for querying.
	 * @param queryString
	 *            query string that must return entry keys
	 * @param queryParameters
	 *            parameters for query execution
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService, int queryLimit,
			Region<Object, V> region, String queryString,
			Object[] queryParameters) throws RegionNotFoundException {
		this(queryService, queryLimit, region, queryString, queryParameters,
				DEFAULT_PAGE_SIZE);
	}

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param queryLimit
	 *            limit on query result
	 * @param region
	 *            The region for querying.
	 * @param queryString
	 *            query string that must return entry keys
	 * @param queryParameters
	 *            parameters for query execution
	 * @param pageSize
	 *            size of page
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService, int queryLimit,
			Region<Object, V> region, String queryString,
			Object[] queryParameters, int pageSize)
			throws RegionNotFoundException {
		this.queryService = queryService;

		this.queryRegion = region;
		if (queryRegion == null) {
			throw new NullPointerException("Query region have to be provided");
		}

		RegionService regionService = queryRegion.getRegionService();
		paginatedQueryInfoRegion = regionService
				.getRegion(PAGINATED_QUERY_INFO_REGION_NAME);

		if (paginatedQueryInfoRegion == null) {
			RegionNotFoundException e = new RegionNotFoundException(
					"Help region ["
							+ PAGINATED_QUERY_INFO_REGION_NAME
							+ "] for storing "
							+ "information about paginated queries has not been found");
			logger.warn(e.getMessage());
			throw e;
		}

		if (pageSize < 5) {
			throw new IllegalArgumentException(
					"Page size must be greater than 4");
		}
		this.pageSize = pageSize;

		if (queryLimit < 1) {
			throw new IllegalArgumentException("Query limit must be positive");
		}
		this.queryLimit = queryLimit;

		this.queryString = CacheUtils.addQueryLimit(queryString,
				this.queryLimit);
		this.queryParams = queryParameters;
	}

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param region
	 *            The region for querying
	 * @param queryString
	 *            query string that must return entry keys
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService, Region<Object, V> region,
			String queryString) throws RegionNotFoundException {
		this(queryService, DEFAULT_QUERY_LIMIT, region, queryString,
				DEFAULT_PAGE_SIZE);
	}

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param region
	 *            The region for querying.
	 * @param queryString
	 *            query string that must return entry keys
	 * @param pageSize
	 *            size of page
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService,
			Region<Object, V> region, String queryString, int pageSize)
			throws RegionNotFoundException {
		this(queryService, DEFAULT_QUERY_LIMIT, region, queryString,
				new Object[] {}, pageSize);
	}

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param region
	 *            The region for querying.
	 * @param queryString
	 *            query string that must return entry keys
	 * @param queryParameters
	 *            parameters for query execution
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService, Region<Object, V> region,
			String queryString, Object[] queryParameters)
			throws RegionNotFoundException {
		this(queryService, DEFAULT_QUERY_LIMIT, region, queryString,
				queryParameters, DEFAULT_PAGE_SIZE);
	}

	/**
	 * Creates a new PaginatedQuery instance.
	 *
	 * @param queryService
	 *            The service to run the query.
	 * @param region
	 *            The region for querying.
	 * @param queryString
	 *            query string that must return entry keys
	 * @param queryParameters
	 *            parameters for query execution
	 * @param pageSize
	 *            size of page
	 * @throws RegionNotFoundException
	 *             when query region or help region were not founded
	 */
	public PaginatedQuery(QueryService queryService, Region<Object, V> region,
			String queryString, Object[] queryParameters, int pageSize)
			throws RegionNotFoundException {
		this(queryService, DEFAULT_QUERY_LIMIT, region, queryString,
				queryParameters, pageSize);
	}

	/**
	 * Returns size of page.
	 *
	 * @return page size
	 */
	public int getPageSize() {
		return this.pageSize;
	}

	/**
	 * Returns a total number of query entries.
	 *
	 * @return total number of entries
	 * @throws com.gemstone.gemfire.cache.query.QueryException
	 *             during query execution
	 */
	public int getTotalNumberOfEntries() throws QueryException {
		prepareResultData(false);
		return totalNumberOfEntries;
	}

	/**
	 * Returns a total number of query pages.
	 *
	 * @return total number of pages
	 * @throws com.gemstone.gemfire.cache.query.QueryException
	 *             during query execution
	 */
	public int getTotalNumberOfPages() throws QueryException {
		prepareResultData(false);
		if (isEmpty()) {
			return 1;
		}
		int total = totalNumberOfEntries / this.pageSize;
		if (totalNumberOfEntries % this.pageSize > 0) {
			total += 1;
		}
		return total;
	}

	/**
	 * Gets value of a flag that indicates excess of query limit.
	 *
	 * @return boolean
	 * @throws com.gemstone.gemfire.cache.query.QueryException
	 *             during query execution
	 */
	public boolean isLimitExceeded() throws QueryException {
		prepareResultData(false);
		return limitExceeded;
	}

	/**
	 * Returns entries for a specified page number. Use getTotalNumberOfPages()
	 * method to know how many pages this query has.
	 *
	 * @param pageNumber
	 *            number of page to return
	 * @throws com.gemstone.gemfire.cache.query.QueryException
	 *             during query execution
	 *
	 * @return List<V> list of entries
	 */
	public List<V> page(int pageNumber) throws QueryException {
		List<Object> pageKeys = null;
		boolean firstTry = true;

		while (pageKeys == null) {
			prepareResultData(!firstTry);

			if (!pageExists(pageNumber)) {
				throw new IndexOutOfBoundsException("The page " + pageNumber
						+ "does not exists. " + +getTotalNumberOfPages()
						+ " pages available.");

			}

			PageKey pageKey = newKey(pageNumber);
			pageKeys = paginatedQueryInfoRegion.get(pageKey);

			if (pageKeys == null && firstTry) {
				firstTry = false;
			} else {
				break;
			}
		}

		if (pageKeys != null) {
			return getValues(pageKeys);
		} else {
			throw new RuntimeException(
					"Unable to load keys from cache. Too aggressive expiration policy?");
		}
	}

	/**
	 * Checks that a specified page number exists.
	 *
	 * @param pageNumber
	 *            of type int
	 * @return boolean
	 * @throws com.gemstone.gemfire.cache.query.QueryException
	 *             during query execution
	 */
	public boolean pageExists(int pageNumber) throws QueryException {
		return pageNumber == 1
				|| !(pageNumber < 1 || pageNumber > getTotalNumberOfPages());
	}

	protected void storePage(int pageNumber, List<Object> page) {
		PageKey pageKey = newKey(pageNumber);
		paginatedQueryInfoRegion.put(pageKey, page);
	}

	private List<Object> extractKeys(SelectResults<Object> results) {
		List<Object> keys = new ArrayList<Object>(results.size());
		if (results.getCollectionType().getElementType().isStructType()) {
			for (Object result : results) {
				Object key;
				try {
					key = ((Struct) result).get("key");
				} catch (IllegalArgumentException e) {
					throw new IllegalArgumentException(
							e.getMessage()
									+ " (hint: maybe you forgot to include entry key into query projection list)");
				}
				keys.add(key);
			}
		} else {
			keys = results.asList();
		}
		return keys;
	}

	/**
	 * Returns values for given keys.
	 *
	 * @param entriesKeysForPage
	 *            of type List<Object>
	 * @return List<V>
	 */
	@SuppressWarnings("unchecked")
	private List<V> getValues(List<Object> entriesKeysForPage) {
		if (entriesKeysForPage.isEmpty()) {
			return Collections.emptyList();
		}
		Map<Object, V> entriesMap = queryRegion.getAll(entriesKeysForPage);

		List<V> entries = new ArrayList<V>(entriesKeysForPage.size());
		for (Object key : entriesKeysForPage) {
			entries.add(entriesMap.get(key));
		}
		return entries;
	}

	/**
	 * Handles throwable exceptions during query execution and replaces them by
	 * checked exception.
	 *
	 * @param e
	 *            of type Throwable
	 * @throws com.gemstone.gemfire.cache.query.QueryException
	 *             checked exception
	 */
	private void handleException(Exception e) throws QueryException {
		throw new QueryException(
				"Exception has been thrown during query execution. "
						+ "Cause exception message: " + e.getMessage(), e);
	}

	/**
	 * Checks that query doesn't have results.
	 *
	 * @return the empty (type boolean) of this PaginatedQuery object.
	 */
	private boolean isEmpty() {
		return totalNumberOfEntries == 0;
	}

	private PageKey newKey(int pageNumber) {
		return new PageKey(this.queryString, this.queryParams, this.queryLimit,
				this.pageSize, pageNumber);
	}

	/**
	 * Stores paginated query info if it has not been stored yet.
	 *
	 * @param force
	 *
	 * @throws com.gemstone.gemfire.cache.query.QueryException
	 *             during query execution
	 */
	@SuppressWarnings({ "unchecked" })
	private void prepareResultData(boolean force) throws QueryException {
		if (this.infoLoaded && !force) {
			return;
		}

		PageKey pageKey = newKey(PAGE_NUMBER_FOR_GENERAL_INFO);

		List<Object> queryInfo = null;
		if (!force) {
			queryInfo = paginatedQueryInfoRegion.get(pageKey);
		}

		if (queryInfo == null) {
			Query query = queryService.newQuery(this.queryString);
			SelectResults<Object> results = null;
			try {
				results = (SelectResults<Object>) query.execute(pageKey
						.getQueryParameters());
			} catch (FunctionDomainException e) {
				handleException(e);
			} catch (TypeMismatchException e) {
				handleException(e);
			} catch (NameResolutionException e) {
				handleException(e);
			} catch (QueryInvocationTargetException e) {
				handleException(e);
			}

			if (results.size() > queryLimit) {
				this.limitExceeded = true;
				this.totalNumberOfEntries = queryLimit;
				String msg = "Size of query results has exceeded limit ("
						+ queryLimit + "). Truncated.";
				logger.warn(msg);
			} else {
				limitExceeded = false;
				this.totalNumberOfEntries = results.size();
			}

			queryInfo = Arrays.asList(new Object[] { results.size(),
					limitExceeded });
			storePage(PAGE_NUMBER_FOR_GENERAL_INFO, queryInfo);

			List<Object> keys = extractKeys(results);
			storeResults(keys);
		} else {
			this.totalNumberOfEntries = (Integer) queryInfo.get(0);
			this.limitExceeded = (Boolean) queryInfo.get(1);
		}
		this.infoLoaded = true;
	}

	/**
	 * Stores paginated query pages and general info.
	 *
	 * @param resultKeys
	 *            of type List<Object>
	 */
	private void storeResults(List<Object> resultKeys) {
		if (resultKeys.size() > queryLimit) {
			resultKeys = resultKeys.subList(0, queryLimit);
		}

		int keyNumber = 0;
		int pageNumber = 0;
		List<Object> page = new ArrayList<Object>();

		for (Object key : resultKeys) {
			if (keyNumber % getPageSize() == 0 && keyNumber != 0) {
				storePage(++pageNumber, page);
				page.clear();
			}
			page.add(key);
			keyNumber++;
		}

		if (page.size() > 0 || pageNumber == 0) {
			storePage(++pageNumber, page);
		}
	}
}


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

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

    /**
     * Default constructor.
     */
    public PortRenderer() {
        super(Port.class);
    }

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

        final String prefix = entity.getId() != null && !entity.getId().isEmpty()
                ? ": " + quote(entity.getId()) + " "
                : "";

        // ALl Done.
        return prefix  + ": " + entity.getCompassPoint().getDotToken();
    }
}


package nl.pwiddershoven.jfactory;

import nl.pwiddershoven.jfactory.annotations.AfterFactoryCreate;
import nl.pwiddershoven.jfactory.annotations.BeforeFactoryCreate;

/**
 * Abstract class that provides functionality for building objects that can be persisted through some persistence layer.
 * The actual persisting of objects is to be implemented by subclasses.
 * @param <T> factory class, see {@link ObjectFactory}
 */
public abstract class PersistableObjectFactory<T> extends ObjectFactory<T> {

    protected abstract void persist(T object);

    public PersistableObjectFactory(Class<T> factoryClass) {
        super(factoryClass);
    }

    /**
     * Create an object with the given attributes.
     * First builds the object, than persists it.
     * @param attributes
     * @return
     */
    public T create(Object... attributes) {
        // build
        T object = build(attributes);

        // excute beforeCreate callback
        executeCallbacks(BeforeFactoryCreate.class, object);

        // persist
        persist(object);

        // execute after create callback
        executeCallbacks(AfterFactoryCreate.class, object);

        // return
        return object;
    }
}