uk.co.q3c.v7.base.navigate.TextReaderSitemapProvider.java Source code

Java tutorial

Introduction

Here is the source code for uk.co.q3c.v7.base.navigate.TextReaderSitemapProvider.java

Source

/*
 * Copyright (C) 2013 David Sowerby
 * 
 * Licensed 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.
 */
package uk.co.q3c.v7.base.navigate;

import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

import javax.inject.Inject;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.apache.shiro.io.ResourceUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.co.q3c.v7.base.view.V7View;
import uk.co.q3c.v7.i18n.CurrentLocale;
import uk.co.q3c.v7.i18n.I18NKey;

import com.google.common.base.Strings;

public class TextReaderSitemapProvider implements SitemapProvider {

    private static Logger log = LoggerFactory.getLogger(TextReaderSitemapProvider.class);

    private enum SectionName {
        options, redirects, viewPackages, standardPageMapping, map;
    }

    private enum ValidOption {
        appendView, labelKeys, generatePublicHomePage, generateAuthenticationPages, generateRequestAccount, generateRequestAccountReset, systemAccountRoot, publicRoot, privateRoot
    }

    private Sitemap sitemap;
    private int commentLines;
    private int blankLines;
    private Map<SectionName, List<String>> sections;

    private SectionName currentSection;

    private Class<? extends Enum<?>> labelKeysClass;
    // options
    private boolean appendView;
    private String labelKeys;

    private Set<String> missingEnums;
    private Set<String> invalidViewClasses;
    private Set<String> undeclaredViewClasses;
    private Set<String> indentationErrors;
    private Set<String> missingPages;
    private Set<String> propertyErrors;
    private Set<String> duplicateURIs;
    private Set<String> viewlessURIs;
    private Set<String> unrecognisedOptions;
    private Set<String> redirectErrors;
    private Set<String> syntaxErrors;
    private Set<String> standardPageErrors;

    // messages to go in the report, for info only
    private Set<String> infoMessages;

    private StringBuilder report;
    private DateTime startTime;
    private DateTime endTime;
    private String source;
    private boolean parsed = false;
    private boolean labelClassNotI18N;
    private boolean labelClassNonExistent;
    private boolean labelClassMissing = true;
    private String labelClassName;

    private File sourceFile;
    private final StandardPageBuilder standardPageBuilder;
    private LabelKeyForName lkfn;
    private final CurrentLocale currentLocale;
    private final Collator collator;

    @Inject
    public TextReaderSitemapProvider(StandardPageBuilder standardPageBuilder, CurrentLocale currentLocale) {
        super();
        this.standardPageBuilder = standardPageBuilder;
        this.currentLocale = currentLocale;
        this.collator = Collator.getInstance(currentLocale.getLocale());

    }

    private void init() {
        startTime = DateTime.now();
        endTime = null;
        missingEnums = new HashSet<>();
        invalidViewClasses = new HashSet<>();
        undeclaredViewClasses = new HashSet<>();
        indentationErrors = new HashSet<>();
        missingPages = new HashSet<>();
        propertyErrors = new HashSet<>();
        viewlessURIs = new HashSet<>();
        duplicateURIs = new HashSet<>();
        unrecognisedOptions = new HashSet<>();
        redirectErrors = new HashSet<>();
        infoMessages = new HashSet<>();
        syntaxErrors = new HashSet<>();
        standardPageErrors = new HashSet<>();

        sitemap = new Sitemap();
        standardPageBuilder.setSitemap(sitemap);
        sections = new HashMap<>();
        labelClassNotI18N = false;
        labelClassNonExistent = false;
        labelClassMissing = true;
        parsed = false;
    }

    @Override
    public void parse(String resourcePath) {
        source = resourcePath;
        log.info("Loading sitemap from {}", source);
        InputStream is;
        try {
            is = ResourceUtils.getInputStreamForPath(resourcePath);
            InputStreamReader isr = new InputStreamReader(is);
            Scanner scanner = new Scanner(isr);
            List<String> lines = new ArrayList<>();
            try {
                while (scanner.hasNextLine()) {
                    lines.add(scanner.nextLine());
                }
            } finally {
                scanner.close();
            }
            processLines(lines);

        } catch (Exception e) {
            log.error("Unable to load site map ", e);
            String report = (parsed) ? getReport().toString() : "failed to parse input, unable to generate report";
            log.debug(report);
        }

    }

    private void processLines(List<String> lines) {
        init();
        int i = 0;
        for (String line : lines) {
            divideIntoSections(line, i);
            i++;
        }

        // can only process if ALL required sections are present
        if (missingSections().size() == 0) {
            processOptions();
            processRedirects();
            // processStandardPages();
            generateStandardPages();
            processMap();
            validateRedirects();
            checkLabelKeys();
            checkViews();
            sitemap.setErrors(errorSum());

            log.info("Sitemap loaded successfully");
            log.debug(sitemap.toString());

        } else {
            log.warn("The site map source is missing these sections: {}", missingSections());
            log.error("Site map failed to process, see previous log warnings for details");
            sitemap.setErrors(errorSum());
        }

        endTime = DateTime.now();
        parsed = true;
        sitemap.setReport(getReport().toString());
    }

    /**
     * indentation errors and + unrecognisedOptions are given as warnings rather than errors
     * 
     * @return
     */
    private int errorSum() {
        int c = missingSections().size() + missingEnums.size() + invalidViewClasses.size();
        c += undeclaredViewClasses.size() + missingPages.size() + propertyErrors.size();
        c += viewlessURIs.size() + duplicateURIs.size() + redirectErrors.size() + syntaxErrors.size()
                + standardPageErrors.size();

        if (getViewPackages() == null || getViewPackages().isEmpty()) {
            c++;
        }
        if (labelClassNotI18N) {
            c++;
        }
        if (labelClassNonExistent) {
            c++;
        }
        if (labelClassMissing) {
            c++;
        }
        return c;
    }

    private int warningSum() {
        int c = unrecognisedOptions.size() + indentationErrors.size();
        return c;
    }

    /**
     * Looks for any URIs without views and captures them in {@link #viewlessURIs} for reporting
     */
    private void checkViews() {
        for (SitemapNode node : sitemap.getAllNodes()) {
            if (node.getViewClass() == null) {
                viewlessURIs.add("uri: \"" + sitemap.uri(node) + "\"");
            }
        }

    }

    private void checkLabelKeys() {
        for (SitemapNode node : sitemap.getAllNodes()) {
            if (node.getLabelKey() == null) {
                labelKeyForName(null, node);
            }
        }
    }

    /**
     * Ensure that redirection targets exist, and that no loops can be created
     */
    private void validateRedirects() {
        Collection<String> uris = sitemap.uris();
        for (String target : getRedirects().values()) {
            if (getRedirects().keySet().contains(target)) {
                redirectErrors.add("'" + target + "' cannot be both a redirect source and redirect target");
            }
            if (!uris.contains(target)) {
                redirectErrors
                        .add("'" + target + "' cannot be a redirect target, it has not been defined as a page");

            }
        }

    }

    /**
     * Generates the standard pages according to the settings of options. See
     * https://sites.google.com/site/q3cjava/sitemap#TOC-options-
     */
    private void generateStandardPages() {
        standardPageBuilder.setLabelKeysClass(labelKeysClass);
        standardPageBuilder.setMissingEnums(missingEnums);
        standardPageBuilder.setStandardPageErrors(standardPageErrors);
        standardPageBuilder.generateStandardPages();
    }

    // /**
    // * Standard pages are added after the page map has been built. This may cause a duplication of urls - if so, that
    // is
    // * captured in {@link #duplicateURLs}. As the map is built from the standard page URLs, it may also cause
    // * intermediate nodes to have no View assigned. This is checked and captured in {@link #viewlessURLs}
    // */
    // private void processStandardPages() {
    //
    // List<String> lines = sections.get(SectionName.standardPageMapping);
    // int i = 1;
    //
    // for (String line : lines) {
    //
    // StandardPageKey pageKey = null;
    // String toUrl = null;
    // String viewName = null;
    //
    // if (!line.contains("=")) {
    // propertyErrors.add("Property must contain an '=' sign at line "
    // + linenum(SectionName.standardPageMapping, i));
    // } else {
    // String[] pair = StringUtils.split(line, "=");
    // String pageKeyName = pair[0].trim();
    //
    // try {
    // pageKey = StandardPageKey.valueOf(pageKeyName);
    // if (pair.length > 1) {
    // if (pair[1].contains(":")) {
    // String[] urlView = StringUtils.split(pair[1], ":");
    // if (pair[1].startsWith(":")) {
    // toUrl = "";
    // viewName = pair[1].replace(":", "");
    // } else {
    // toUrl = urlView[0].trim();
    // if (urlView.length > 1) {
    // viewName = urlView[1].trim();
    // }
    // }
    //
    // } else {
    // toUrl = pair[1].trim();
    // }
    //
    // standardPages().put(pageKey, toUrl);
    // } else {
    // standardPages().put(pageKey, "");
    // }
    // } catch (Exception e) {
    // propertyErrors.add(pageKeyName + " is not a valid " + StandardPageKey.class.getSimpleName()
    // + linenum(SectionName.standardPageMapping, i));
    //
    // }
    //
    // }
    //
    // // we now have defined a node, add it to the map
    // // but only if url is there
    // if (toUrl != null) {
    // SitemapNode node = sitemap.append(toUrl);
    // node.setLabelKey(pageKey);
    // // and set the view
    // findView(node, node.getUrlSegment(), viewName);
    // }
    // i++;
    // }
    //
    // // check for missing standard pages
    // for (StandardPageKey spk : StandardPageKey.values()) {
    // if (!standardPages().containsKey(spk)) {
    // missingPages.add(spk.name());
    // }
    // }
    //
    // }

    // private String linenum(SectionName sectionName, int i) {
    // return "at line " + i + " in the " + sectionName + " section";
    // }

    @Override
    public void parse(File file) {

        source = file.getAbsolutePath();
        sourceFile = file;
        log.info("Loading sitemap from {}", source);
        try {
            List<String> lines = FileUtils.readLines(file);
            processLines(lines);

        } catch (Exception e) {
            log.error("Unable to load site map", e);
            String report = (parsed) ? getReport().toString() : "failed to parse input, unable to generate report";
            log.debug(report);
        }
    }

    private void processRedirects() {
        List<String> sectionLines = sections.get(SectionName.redirects);
        for (String line : sectionLines) {
            // if starts with ':' then f==""
            // split the line on ':'

            if (line.startsWith(":")) {
                getRedirects().put("", line.replace(":", "").trim());
            } else {
                String[] pair = null;
                pair = StringUtils.split(line, ":");
                String f = pair[0].trim();
                String t = (pair.length > 1) ? pair[1].trim() : "";
                getRedirects().put(f, t);
            }
        }
    }

    private void processOptions() {
        List<String> sectionLines = sections.get(SectionName.options);
        String sectionName = SectionName.options.name();
        int i = 1;
        for (String line : sectionLines) {
            if (!line.contains("=")) {
                propertyErrors.add(
                        "Property must contain an '=' sign at line " + i + " in the " + sectionName + " section");
            } else {
                // split the line on '='
                String[] pair = StringUtils.split(line, "=");
                String key = pair[0].trim();

                // malformed property may not have anything after the '='
                String value = (pair.length > 1) ? pair[1].trim() : null;

                // check for empty key or value
                boolean valid = true;

                if (Strings.isNullOrEmpty(key)) {
                    propertyErrors
                            .add("Property must have a key at line " + i + " in the " + sectionName + " section");
                    valid = false;
                } else {
                    if (Strings.isNullOrEmpty(value)) {
                        propertyErrors.add("Property " + key + " cannot have an empty value");
                        valid = false;
                    }
                }

                // process valid properties only
                if (valid) {
                    setOption(key, value);
                }
            }
            // increment line count
            i++;
        }
    }

    private void setOption(String key, String value) {

        try {
            ValidOption k = ValidOption.valueOf(key);
            switch (k) {
            case appendView:
                appendView = "true".equals(value);
                break;
            case generateAuthenticationPages:
                standardPageBuilder.setGenerateAuthenticationPages("true".equals(value));
                break;
            case generatePublicHomePage:
                standardPageBuilder.setGeneratePublicHomePage("true".equals(value));
                break;
            case generateRequestAccount:
                standardPageBuilder.setGenerateRequestAccount("true".equals(value));
                break;
            case generateRequestAccountReset:
                standardPageBuilder.setGenerateRequestAccountReset("true".equals(value));
                break;
            case labelKeys:
                labelKeys = value;
                if (!Strings.isNullOrEmpty(value)) {
                    labelClassMissing = false;
                    labelClassName = value;
                    validateLabelKeys();
                }
                break;
            case systemAccountRoot:
                setSystemAccountRoot(value);
                break;
            case privateRoot:
                sitemap.setPrivateRoot(value);
                break;

            case publicRoot:
                sitemap.setPublicRoot(value);
                break;
            }

        } catch (Exception e) {
            log.warn("unrecognised option '{}' in site map", key);
            unrecognisedOptions.add(key);
        }

    }

    private void setSystemAccountRoot(String systemAccountRoot) {
        standardPageBuilder.setSystemAccountRoot(systemAccountRoot);

    }

    @SuppressWarnings("unchecked")
    private void validateLabelKeys() {
        boolean valid = true;
        Class<?> requestedLabelKeysClass = null;
        try {

            requestedLabelKeysClass = Class.forName(labelKeys);
            // enum
            if (!requestedLabelKeysClass.isEnum()) {
                valid = false;
            }

            // instance of I18NKeys
            @SuppressWarnings("rawtypes")
            Class<I18NKey> i18nClass = I18NKey.class;
            if (!i18nClass.isAssignableFrom(requestedLabelKeysClass)) {
                valid = false;
                labelClassNotI18N = true;
                log.warn(labelKeys + " does not implement I18NKeys");
            }
        } catch (ClassNotFoundException e) {
            valid = false;
            labelClassNonExistent = true;
            log.warn(labelKeys + " does not exist on the classpath");
        }
        if (!valid) {
            log.warn(labelKeys + " is not a valid enum class for I18N labels");
            this.labelClassNotI18N = true;
        } else {
            labelKeysClass = (Class<? extends Enum<?>>) requestedLabelKeysClass;
            lkfn = new LabelKeyForName(labelKeysClass);
        }
    }

    public List<String> getViewPackages() {
        return sections.get(SectionName.viewPackages);
    }

    private void processMap() {
        URITracker uriTracker = new URITracker();
        MapLineReader reader = new MapLineReader();
        List<String> sectionLines = sections.get(SectionName.map);
        int lineIndex = 1;
        int currentIndent = 0;
        for (String line : sectionLines) {
            MapLineRecord lineRecord = reader.processLine(lineIndex, line, syntaxErrors, indentationErrors,
                    currentIndent);
            uriTracker.track(lineRecord.getIndentLevel(), lineRecord.getSegment());
            SitemapNode node = sitemap.append(uriTracker.uri());
            // if node is a standard page do not overwrite it
            if (node.getLabelKey() instanceof StandardPageKey) {
                // warning
            } else {
                node.setUriSegment(lineRecord.getSegment());
                findView(node, lineRecord.getSegment(), lineRecord.getViewName());
                labelKeyForName(lineRecord.getKeyName(), node);
            }
            currentIndent = lineRecord.getIndentLevel();
            lineIndex++;
        }
    }

    // private void processMap() {
    // List<String> sectionLines = sections.get(SectionName.map);
    // int i = 0;
    // SitemapNode currentNode = null;
    // int currentLevel = 0;
    // for (String line : sectionLines) {
    // if (line.startsWith("-")) {
    // int treeLevel = lastIndent(line);
    // int viewStart = line.indexOf(":");
    // int labelStart = line.indexOf("~");
    // String segment = null;
    // String view = null;
    // String labelKeyName = null;
    // if ((labelStart > 0) && (viewStart > 0)) {
    // if (viewStart < labelStart) {
    // segment = line.substring(treeLevel, viewStart);
    // view = line.substring(viewStart + 1, labelStart);
    // labelKeyName = line.substring(labelStart + 1);
    // } else {
    // segment = line.substring(treeLevel, labelStart);
    // labelKeyName = line.substring(labelStart + 1, viewStart);
    // view = line.substring(viewStart + 1);
    // }
    // } else {
    // // only label
    // if (labelStart > 0) {
    // segment = line.substring(treeLevel, labelStart);
    // labelKeyName = line.substring(labelStart + 1);
    // }// only view
    // else if (viewStart > 0) {
    // segment = line.substring(treeLevel, viewStart);
    // view = line.substring(viewStart + 1);
    // }
    // // only segment
    // else {
    // segment = line.substring(treeLevel);
    // }
    // }
    //
    // // segment has been set, view & label may be null
    // SitemapNode node = new SitemapNode();
    // node.setUriSegment(segment);
    //
    // // do structure before labels
    // // labels are not needed for redirected pages
    // // but we cannot get full URI until structure done
    //
    // // add the node
    // if (treeLevel == 1) {
    // // at level 1 each becomes a 'root' (technically the site
    // // tree is a forest)
    // sitemap.addNode(node);
    // currentNode = node;
    // currentLevel = treeLevel;
    // } else {
    // // if indent going back up tree, walk up from current node
    // // to the parent level needed
    // if (treeLevel < currentLevel) {
    // int retraceLevels = currentLevel - treeLevel;
    // for (int k = 1; k <= retraceLevels; k++) {
    // currentNode = sitemap.getParent(currentNode);
    // currentLevel--;
    // }
    // sitemap.addChild(currentNode, node);
    // currentNode = node;
    // currentLevel++;
    // } else if (treeLevel == currentLevel) {
    // SitemapNode parentNode = sitemap.getParent(currentNode);
    // sitemap.addChild(parentNode, node);
    // } else if (treeLevel > currentLevel) {
    // if (treeLevel - currentLevel > 1) {
    // log.warn(
    // "indentation for {} line is too great.  It should be a maximum of 1 greater than its predecessor",
    // node.getUriSegment());
    // indentationErrors.add(node.getUriSegment());
    // }
    // sitemap.addChild(currentNode, node);
    // currentNode = node;
    // currentLevel++;
    // }
    //
    // }
    //
    // String uri = sitemap.uri(node);
    // // do the view
    // if (!getRedirects().containsKey(uri)) {
    // findView(node, segment, view);
    // }
    //
    // // do the label
    // labelKeyForName(labelKeyName, node);
    //
    // } else {
    // String msg = "line in map must start with a'-', line " + i;
    // log.warn(msg);
    // syntaxErrors.add(msg);
    // }
    // }
    //
    // }

    public void labelKeyForName(String labelKeyName, SitemapNode node) {
        // gets name from segment if necessary
        String keyName = keyName(labelKeyName, node);
        // could be null if invalid label keys given
        if (lkfn != null) {
            node.setLabelKey(lkfn.keyForName(keyName, missingEnums), currentLocale.getLocale(), collator);
        } else {
            missingEnums.add(keyName);
        }
    }

    public String keyName(String labelKeyName, SitemapNode node) {
        String keyName = labelKeyName;
        if (keyName == null) {
            keyName = node.getUriSegment().replace("-", " ");
            keyName = keyName.replace("_", " ");
            keyName = WordUtils.capitalize(keyName);
            // hyphen not valid in enum, but may be used in segment
            keyName = keyName.replace(" ", "_");
            return keyName;
        } else {
            return keyName;
        }
    }

    /**
     * Updates the node with the required view. If {@link #appendView} is true the 'View' is appended to the
     * {@code viewName} before attempting to find its class declaration. If no class can be found, {@code viewName} is
     * added to {@link #undeclaredViewClasses}
     * 
     * @param node
     * @param segment
     * @param viewName
     */
    @SuppressWarnings("unchecked")
    private void findView(SitemapNode node, String segment, String viewName) {
        // if view is null use the segment
        if (viewName == null) {
            viewName = StringUtils.capitalize(segment);
        }

        // user option whether to append 'View' or not
        if (appendView) {
            viewName = viewName + "View";
        }
        Class<?> viewClass = null;
        // try and find the view in the specified packages
        for (String pkg : getViewPackages()) {
            String fullViewName = pkg + "." + viewName;
            try {
                viewClass = Class.forName(fullViewName);
                if (V7View.class.isAssignableFrom(viewClass)) {
                    node.setViewClass((Class<V7View>) viewClass);
                    break;
                } else {
                    invalidViewClasses.add(fullViewName);
                }
            } catch (ClassNotFoundException e) {
                // don't need to do anything
            }

        }
        if (viewClass == null) {
            undeclaredViewClasses.add(viewName);
        }

    }

    private int lastIndent(String line) {
        int index = 0;
        while (line.charAt(index) == '-') {
            index++;
        }
        return index;
    }

    /**
     * process a line of text from the file into the appropriate section
     * 
     * @param line
     */
    private void divideIntoSections(String line, int linenum) {
        String strippedLine = StringUtils.deleteWhitespace(line);
        if (strippedLine.startsWith("#")) {
            commentLines++;
            return;
        }
        if (Strings.isNullOrEmpty(strippedLine)) {
            blankLines++;
            return;
        }
        if (strippedLine.startsWith("[")) {
            if ((!strippedLine.endsWith("]"))) {
                log.warn("section requires closing ']' at line " + linenum);
            } else {
                String sectionName = strippedLine.substring(1, strippedLine.length() - 1);

                List<String> section = new ArrayList<>();
                try {
                    SectionName key = SectionName.valueOf(sectionName);
                    currentSection = key;
                    sections.put(key, section);
                } catch (IllegalArgumentException iae) {
                    log.warn(
                            "Invalid section '{}' in site map file, this section has been ignored. Only sections {} are allowed.",
                            sectionName, getSections().toString());
                }

            }
            return;
        }

        List<String> section = sections.get(currentSection);
        if (section != null) {
            section.add(strippedLine);
        }

    }

    @Override
    public Sitemap getSitemap() {
        return sitemap;
    }

    public int getCommentLines() {
        return commentLines;
    }

    public int getBlankLines() {
        return blankLines;
    }

    public Set<String> getSections() {
        Set<String> sections = new TreeSet<>();
        for (SectionName sectionName : SectionName.values()) {
            sections.add(sectionName.name());
        }
        return sections;
    }

    public String getLabelKeys() {
        return labelKeys;
    }

    public boolean isAppendView() {
        return appendView;
    }

    @SuppressWarnings("rawtypes")
    public Class<? extends Enum> getLabelKeysClass() {
        return labelKeysClass;
    }

    public Set<String> getMissingEnums() {
        return missingEnums;
    }

    private void buildReport() {
        report = new StringBuilder();
        String df = "dd MMM YYYY HH:mm:SS";

        report.append("==================== SiteMap builder report ==================== \n\n");
        report.append("parsing source from:\t\t");
        report.append(source);
        report.append("\n\n");

        report.append("start at:\t\t\t");
        report.append(startTime.toString(df));
        report.append("\n");

        report.append("end at:\t\t\t\t");
        report.append(endTime.toString(df));
        report.append("\n");

        report.append("run time:\t\t\t");
        report.append(runtime().toString());
        report.append(" ms\n\n");

        report.append("pages defined:\t\t\t");
        report.append(getPagesDefined());
        report.append("\n\n");

        report.append("redirects:\t\t\t");
        for (String entry : redirectEntries()) {
            report.append("\n -- ");
            report.append(entry);
        }
        report.append("\n\n");

        if (getViewPackages() != null) {
            report.append("view packages declared:\t\t");
            report.append(getViewPackages().toString());
            report.append("\n\n");
        }

        if (!(labelClassMissing || labelClassNonExistent || labelClassNotI18N)) {
            report.append("I18N Label class:\t\t");
            report.append(labelClassName);
            report.append("\n\n");
        }

        report.append("parsing status:  ");
        if (sitemap.hasErrors()) {
            report.append("FAILED");
        } else {
            report.append("PASSED");
        }

        report.append("\n\n");

        if (sitemap.hasErrors()) {
            report.append(" -------- errors --------\n\n");
        }
        reportChunk(missingSections(), "missing sections",
                "if any section is missing, parsing will fail, and results will be indeterminate - correct this first");

        if (getViewPackages() == null) {
            report.append("No view packages declared - site map will not build without them\n\n");
        }

        if (labelClassMissing || labelClassNonExistent || labelClassNotI18N) {
            report.append("I18N Label class:\t\t");
            if (labelClassMissing) {
                report.append(
                        " has not been declared, you need to define it using the 'labelKeys=' property in [options]");
                report.append("\n\n");
            } else {
                if (labelClassNonExistent) {
                    report.append(labelClassName);
                    report.append(" has been declared but does not exist on the classpath");
                    report.append("\n\n");
                } else {
                    if (labelClassNotI18N) {
                        report.append(labelClassName);
                        report.append(
                                " has been declared, is on the classpath, but does not implement I18NKeys, as it should");
                        report.append("\n\n");
                    }
                }
            }
        }

        reportChunk(missingPages, "missing pages", "these MUST be defined");
        reportChunk(propertyErrors, "property errors", "should be key=value, spaces are ignored");
        reportChunk(missingEnums, "missing enum declarations",
                "you could just paste these into your enum declaration");
        reportChunk(invalidViewClasses, "invalid view classes", "invalid because they do not implement V7View");
        reportChunk(undeclaredViewClasses, "undeclared view classes",
                "these could not be found in the view packages declared in the [viewPackages] section");

        reportChunk(syntaxErrors, "syntax errors",
                "these have been ignored, and the system may work, but you may not get the intended result", true);
        reportChunk(redirectErrors, "redirect errors", "Redirect(s) causing an inconsistency and must be fixed",
                true);
        reportChunk(viewlessURIs, "viewless URIs", "these URIs have no view associated with them", true);
        reportChunk(standardPageErrors, "standard page errors",
                "incomplete or incorrect defintion of standard pages in [standardPageMapping]", true);

        if (warningSum() > 0) {
            report.append(" --------------- warnings ---------");
            report.append("\n\n");
            reportChunk(indentationErrors, "indentation errors",
                    "line indentation should be <= 1 greater than the preceding line.  Parsing will still work but you may not get the intended result");
            reportChunk(unrecognisedOptions, "unrecognised options",
                    "these have just been ignored, will do no harm");
        }

        report.append("================================================================= ");

    }

    private void reportChunk(Set<String> source, String name, String explain) {
        reportChunk(source, name, explain, false);
    }

    private void reportChunk(Set<String> source, String name, String explain, boolean multiline) {
        if (source.size() > 0) {
            report.append(name);
            report.append("\t\t");
            report.append(source.size());
            report.append("  (");
            report.append(explain);
            report.append(")\n");
            if (source.size() > 0) {
                if (multiline) {
                    for (String s : source) {
                        report.append("\t");
                        report.append(s);
                        report.append("\n");
                    }
                } else {
                    report.append("  -- ");
                    report.append(source);
                    report.append("\n");
                }
            }
            report.append("\n");
        }
    }

    public int getPagesDefined() {
        return getSitemap().getNodeCount();
    }

    public Long runtime() {
        Long r = endTime.getMillis() - startTime.getMillis();
        return r;
    }

    public DateTime getStartTime() {
        return startTime;
    }

    public DateTime getEndTime() {
        return endTime;
    }

    @Override
    public StringBuilder getReport() {
        if (!parsed) {
            throw new SiteMapException("File must be parsed before report is requested");
        }
        buildReport();
        return report;
    }

    public void setEndTime(DateTime endTime) {
        this.endTime = endTime;
    }

    public Set<String> missingSections() {
        Set<String> missing = new HashSet<>();
        for (SectionName section : SectionName.values()) {
            if (!sections.containsKey(section)) {
                missing.add(section.name());
            }
        }
        return missing;
    }

    public Set<String> getInvalidViewClasses() {
        return invalidViewClasses;
    }

    public Set<String> getUndeclaredViewClasses() {
        return undeclaredViewClasses;
    }

    public boolean isLabelClassNotI18N() {
        return labelClassNotI18N;
    }

    public boolean isLabelClassNonExistent() {
        return labelClassNonExistent;
    }

    public Set<String> getIndentationErrors() {
        return indentationErrors;
    }

    /**
     * If sitemap has already been parsed, returns it. If not, loads input and parses it. The source depends on what has
     * been set in {@link #source} and {@link #sourceFile}. The first available source is taken from the following
     * order:
     * <ol>
     * <li>source
     * <li>sourceFile
     * <li>default (which is "classpath:sitemap.properties")
     * 
     * @see uk.co.q3c.v7.base.navigate.SitemapProvider#get()
     */

    @Override
    public Sitemap get() {
        if (parsed) {
            return getSitemap();
        }
        if (source != null) {
            parse(source);
        } else {
            if (sourceFile != null) {
                parse(sourceFile);
            } else {
                parse("classpath:sitemap.properties");
            }
        }

        return getSitemap();
    }

    public String standardPageUri(StandardPageKey key) {
        return standardPages().get(key);
    }

    public Map<StandardPageKey, String> standardPages() {
        return sitemap.getStandardPages();
    }

    public Set<String> getMissingPages() {
        return missingPages;
    }

    public Set<String> getPropertyErrors() {
        return propertyErrors;
    }

    public Map<String, String> getRedirects() {
        return sitemap.getRedirects();
    }

    public Set<String> getViewlessURIs() {
        return viewlessURIs;
    }

    public Set<String> getDuplicateURIs() {
        return duplicateURIs;
    }

    public Set<String> redirectEntries() {
        Set<String> ss = new HashSet<>();
        for (Map.Entry<String, String> entry : getRedirects().entrySet()) {
            ss.add(entry.getKey() + ":" + entry.getValue());
        }
        return ss;
    }

    public boolean isLabelClassMissing() {
        return labelClassMissing;
    }

    public File getSourceFile() {
        return sourceFile;
    }

    /**
     * 
     * Sets the source of the sitemap input. See also {@link #setSource(String)} , and {@link #get()} for loading order.
     * 
     * @param sourceFile
     */
    public void setSourceFile(File sourceFile) {
        this.sourceFile = sourceFile;
    }

    public String getSource() {
        return source;
    }

    /**
     * Sets the source of the sitemap input. Must be in the format of
     * {@link ResourceUtils#getInputStreamForPath(String)}. See also {@link #setSourceFile(File)}, and {@link #get()}
     * for loading order.
     * 
     * @param source
     */
    public void setSource(String source) {
        this.source = source;
    }

    public Set<String> getRedirectErrors() {
        return redirectErrors;
    }

    public Set<String> getSyntaxErrors() {
        return syntaxErrors;
    }

    public Set<String> getInfoMessages() {
        return infoMessages;
    }

    public boolean isGeneratePublicHomePage() {
        return standardPageBuilder.isGeneratePublicHomePage();
    }

    public boolean isGenerateAuthenticationPages() {
        return standardPageBuilder.isGenerateAuthenticationPages();
    }

    public boolean isGenerateRequestAccount() {
        return standardPageBuilder.isGenerateRequestAccount();
    }

    public boolean isGenerateRequestAccountReset() {
        return standardPageBuilder.isGenerateRequestAccountReset();
    }

    public String getSystemAccountUri() {
        return standardPageBuilder.getSystemAccountRoot();
    }

}