Java tutorial
/* * 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.q3c.krail.core.navigate.sitemap; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.text.WordUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.q3c.krail.core.navigate.LabelKeyForName; import uk.q3c.krail.core.navigate.URITracker; import uk.q3c.krail.core.view.KrailView; import uk.q3c.krail.i18n.CurrentLocale; import uk.q3c.krail.i18n.I18NKey; import uk.q3c.krail.i18n.Translate; import java.io.File; import java.text.Collator; import java.time.Duration; import java.time.LocalDateTime; import java.util.*; /** * Loads the {@link MasterSitemap} with the entries contained in the files defined by subclasses of * {@link FileSitemapModule} * * @author David Sowerby */ public class DefaultFileSitemapLoader extends SitemapLoaderBase implements FileSitemapLoader { private enum SectionName { options, viewPackages, map, redirects; } private enum ValidOption { appendView, labelKeys } private static Logger log = LoggerFactory.getLogger(DefaultFileSitemapLoader.class); private final MasterSitemap sitemap; private final Collator collator; private final Translate translate; private final String segmentSeparator = ";"; // options private boolean appendView; private int blankLines; private int commentLines; private SectionName currentSection; private LocalDateTime endTime; private String labelKey; private Class<? extends Enum<?>> labelKeysClass; private LabelKeyForName lkfn; private Set<String> missingEnums; private Map<SectionName, List<String>> sections; private File sourceFile; private Map<String, SitemapFile> sources; private LocalDateTime startTime; @Inject public DefaultFileSitemapLoader(CurrentLocale currentLocale, Translate translate, MasterSitemap sitemap) { super(); this.collator = Collator.getInstance(currentLocale.getLocale()); this.translate = translate; this.sitemap = sitemap; } private void init() { startTime = LocalDateTime.now(); endTime = null; missingEnums = new HashSet<>(); sections = new HashMap<>(); } private void processLines(String source, List<String> lines) { init(); int i = 0; for (String line : lines) { divideIntoSections(source, line, i); i++; } // can only process if ALL required sections are present if (missingSections().size() == 0) { processOptions(source); processMap(source); checkLabelKeys(); processRedirects(); } else { addError(source, SECTION_MISSING, missingSections()); } endTime = LocalDateTime.now(); } /** * The expected syntax of a redirect is <em> fromPage : toPage</em> * <p/> * Lines which do not contain a ':' are ignored<br> * Lines containing multiple ':' will only process the first two segments, the rest is ignored */ private void processRedirects() { List<String> sectionLines = sections.get(SectionName.redirects); for (String redirect : sectionLines) { if (redirect.contains(":")) { Splitter splitter = Splitter.on(":").trimResults(); Iterable<String> split = splitter.split(redirect); Iterator<String> iter = split.iterator(); String fromPage = iter.next(); String toPage = iter.next(); sitemap.addRedirect(fromPage, toPage); } else { addInfo(REDIRECT_INVALID, redirect); } } } private void checkLabelKeys() { for (MasterSitemapNode node : sitemap.getAllNodes()) { if (node.getLabelKey() == null) { labelKeyForName(null, node); } } } private void parse(File file) { init(); sourceFile = file; log.info("Loading sitemap from {}", file.getAbsolutePath()); try { List<String> lines = FileUtils.readLines(file); processLines(file.getAbsolutePath(), lines); } catch (Exception e) { log.error("Unable to load site map", e); } } private void processOptions(String source) { List<String> sectionLines = sections.get(SectionName.options); String sectionName = SectionName.options.name(); int i = 1; for (String line : sectionLines) { if (!line.contains("=")) { addError(source, PROPERTY_MISSING_EQUALS, i, sectionName); } 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)) { addError(source, PROPERTY_MISSING_KEY, i, sectionName); valid = false; } else { if (Strings.isNullOrEmpty(value)) { addError(source, PROPERTY_MISSING_VALUE); valid = false; } } // process valid properties only if (valid) { setOption(source, key, value); } } // increment line count i++; } } private void setOption(String source, String key, String value) { try { ValidOption k = ValidOption.valueOf(key); switch (k) { case appendView: appendView = "true".equals(value); break; case labelKeys: labelKey = value; if (!Strings.isNullOrEmpty(value)) { validateLabelKeys(source); } break; } } catch (Exception e) { addWarning(source, PROPERTY_NAME_UNRECOGNISED, key); } } @SuppressWarnings("unchecked") private void validateLabelKeys(String source) { boolean valid = true; Class<?> requestedLabelKeysClass = null; try { requestedLabelKeysClass = Class.forName(labelKey); // enum if (!requestedLabelKeysClass.isEnum()) { valid = false; } // instance of I18NKeys @SuppressWarnings("rawtypes") Class<I18NKey> i18nClass = I18NKey.class; if (!i18nClass.isAssignableFrom(requestedLabelKeysClass)) { valid = false; addError(source, LABELKEY_DOES_NOT_IMPLEMENT_I18N_KEY, labelKey); } } catch (ClassNotFoundException e) { valid = false; addError(source, LABELKEY_NOT_IN_CLASSPATH, labelKey); } if (!valid) { addError(source, LABELKEY_NOT_VALID_CLASS_FOR_I18N_LABELS, labelKey); } else { labelKeysClass = (Class<? extends Enum<?>>) requestedLabelKeysClass; lkfn = new LabelKeyForName(labelKeysClass); } } public List<String> getViewPackages() { return sections.get(SectionName.viewPackages); } private void processMap(String source) { 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(this, source, lineIndex, line, currentIndent, segmentSeparator); uriTracker.track(lineRecord.getIndentLevel(), lineRecord.getSegment()); MasterSitemapNode node = sitemap.append(uriTracker.uri()); node.setUriSegment(lineRecord.getSegment()); findView(source, node, lineRecord.getSegment(), lineRecord.getViewName()); labelKeyForName(lineRecord.getKeyName(), node); Splitter splitter = Splitter.on(",").trimResults(); Iterable<String> roles = splitter.split(lineRecord.getRoles()); for (String role : roles) { node.addRole(role); } node.setPageAccessControl(lineRecord.getPageAccessControl()); currentIndent = lineRecord.getIndentLevel(); lineIndex++; } } public void labelKeyForName(String labelKeyName, MasterSitemapNode 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)); } else { missingEnums.add(keyName); } } public String keyName(String labelKeyName, SitemapNode node) { String keyName = labelKeyName; if (Strings.isNullOrEmpty(keyName)) { 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(String source, MasterSitemapNode node, String segment, String viewName) { // if view is null use the segment if (Strings.isNullOrEmpty(viewName)) { String s = segment.replace("-", " "); s = WordUtils.capitalize(s); viewName = s.replace(" ", "_"); } // 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 (KrailView.class.isAssignableFrom(viewClass)) { node.setViewClass((Class<KrailView>) viewClass); break; } else { addError(source, VIEW_DOES_NOT_IMPLEMENT_KRAILVIEW, fullViewName); } } catch (ClassNotFoundException e) { // don't need to do anything } } if (viewClass == null) { addError(source, VIEW_NOT_FOUND_IN_SPECIFIED_PACKAGES, 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 source, 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("]"))) { addWarning(source, SECTION_MISSING_CLOSING, 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) { addWarning(source, SECTION_NOT_VALID_FOR_SITEMAP, sectionName, getSections().toString()); } } return; } List<String> section = sections.get(currentSection); if (section != null) { section.add(strippedLine); } } 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 getLabelKey() { return labelKey; } public boolean isAppendView() { return appendView; } @SuppressWarnings("rawtypes") public Class<? extends Enum> getLabelKeysClass() { return labelKeysClass; } public Set<String> getMissingEnums() { return missingEnums; } public int getPagesDefined() { return sitemap.getNodeCount(); } public int runtime() { int diffInNano = Duration.between(startTime, endTime).getNano(); return diffInNano; } public LocalDateTime getStartTime() { return startTime; } public LocalDateTime getEndTime() { return endTime; } public void setEndTime(LocalDateTime 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 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 MasterSitemap getSitemap() { return sitemap; } @Override public boolean load() { if ((sources != null) && (!sources.isEmpty())) { for (SitemapFile source : sources.values()) { parse(new File(source.getFilePath())); StringBuilder buf = new StringBuilder(); boolean first = true; if (!missingEnums.isEmpty()) { for (String e : missingEnums) { if (!first) { buf.append(','); } buf.append(e); first = false; } if (labelKeysClass != null) { addError(source.getFilePath(), ENUM_MISSING, buf.toString(), labelKeysClass.getName()); } } } return true; } else { log.info("No file based sources for the Sitemap identified, nothing to load"); return false; } } public ImmutableMap<String, SitemapFile> getSources() { return ImmutableMap.copyOf(sources); } @Inject(optional = true) public void setSources(Map<String, SitemapFile> sources) { this.sources = sources; } @Override public void addError(String source, String msgPattern, Object... msgParams) { super.addError(source, msgPattern, msgParams); } @Override public void addWarning(String source, String msgPattern, Object... msgParams) { super.addWarning(source, msgPattern, msgParams); } @Override public void addInfo(String source, String msgPattern, Object... msgParams) { super.addInfo(source, msgPattern, msgParams); } public List<String> getSourceNames() { List<String> sourceNames = new ArrayList<>(); for (SitemapFile source : sources.values()) { sourceNames.add(source.getFilePath()); } return sourceNames; } }