/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.maven.tools; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.geotools.maven.taglet.Source; /** * This program adds the @source tag to class header javadocs. Optionally, it will also * replace an existing @source tag with a new one. The tag is used to generate the module * module listing in the class javadocs. For example, this tag: * <pre><code> * @source http://svn.osgeo.org/geotools/trunk/modules/library/api/src/main/java/org/geotools/data/DataStore.java * </code></pre> * Will result in this content at the end of the class header javadocs: * <p> * <b>Module:</b><br> *     modules/library/api (gt-api.jar)<br> * <b>Source repository:</b><br> *     http://svn.osgeo.org/geotools/trunk/modules/library/api/src/main/java/org/geotools/data/DataStore.java * <p> * To run this program on the command line using Maven: * <pre><code> * cd /topgtdir/build/maven/javadoc * mvn exec:java -Dexec.args="path-to-module-src-dir options" * </code></pre> * Where<br> * * {@code path-to-module-src-dir} is a full or relative path to the module's top src directory<br> * * {@code options} is zero or more of the following:<br> * * {@code --replace} to force replacement of existing source tags (default is no replacement)<br> * * {@code --anyclass} process all classes, interfaces and enums (default is only those * that are public)<br> * * {@code --svn} add Subversion delimiters ($URL, $) to the source path to enable auto-updating * when the file is committed using svn (default is no delimiters)<br> * * {@code --fix} attempt to fix existing source tags that have been incorrectly broken across lines * (default is just report broken tags) * * <p> * Adapted from the CommentUpdater class previously in this package that was written * by Martin Desruisseaux. * * @author Michael Bedward * @source $URL$ * @version $Id$ */ public class InsertSourceTag { private final Pattern findSVNLine = Pattern.compile(".+\\/(trunk|tags|branches)\\/.*\\.java"); private final Pattern findJavadocStart = Pattern.compile("^\\s*\\Q/**\\E"); private final Pattern findCommentStart = Pattern.compile("^\\s*\\Q/*\\E([^\\*]|$)"); private final Pattern findCommentEnd = Pattern.compile("\\Q*/\\E"); private final Pattern findSourceTag = Pattern.compile("^.*?\\Q@source\\E"); private final Pattern findCompleteSourceTag = Pattern.compile( "^.*?\\Q@source\\E(.*?)\\Q.java\\E\\s*\\$?"); private final Pattern findCompletePath = Pattern.compile( "^.*?http.*?\\Q.java\\E\\s*\\$?"); private final Pattern findVersionTag = Pattern.compile("^.*?\\Q@version\\E"); private final Pattern findPublicClass = Pattern.compile( "\\s*public[a-zA-Z\\s]+(class|interface|enum)"); private final Pattern findClass = Pattern.compile(".*?(class|interface|enum)"); private final Pattern findAnnotation = Pattern.compile("^@[a-zA-Z]+"); private final String lineSeparator = System.getProperty("line.separator", "\n"); private static final String REPLACE_OPTION = "--replace"; private boolean optionReplace; private static final String SVN_OPTION = "--svn"; private boolean optionSVNDelims; private static final String ANY_CLASS_OPTION = "--anyclass"; private boolean optionAnyClass; private static final String FIX_BROKEN_TAGS = "--fix"; private boolean optionFixBreaks; /** * Main method. Takes the name of the file or directory to process from the * first command line argument provided (only the first is examined). If a * directory, all child directories and java source files will be processed. * <p> * Note: local backup files are <b>not</b> saved by this program. */ public static void main(String[] args) { if (args.length == 0) { System.out.println("usage: InsertSourceTag {options} fileOrDirName"); System.out.println("options:"); System.out.println(" " + REPLACE_OPTION + ": Replaces existing source tags (default no replacement)"); System.out.println(" " + SVN_OPTION + ": Add the svn URL keyword (omitted by default)"); System.out.println(" " + ANY_CLASS_OPTION + ": Process any class (default is only public classes)"); return; } File inputPath = null; InsertSourceTag me = new InsertSourceTag(); for (String s : args) { s = s.trim(); if (s.startsWith("--")) { if (REPLACE_OPTION.equals(s)) { me.optionReplace = true; } else if (SVN_OPTION.equals(s)) { me.optionSVNDelims = true; } else if (ANY_CLASS_OPTION.equals(s)) { me.optionAnyClass = true; } else if (FIX_BROKEN_TAGS.equals(s)) { me.optionFixBreaks = true; } else { System.out.println("Unrecognized option: " + s); return; } } else { // not an option, treat as input path if (inputPath == null) { inputPath = new File(s); if (!inputPath.exists()) { System.out.println("Can't find " + inputPath); return; } } else { System.out.println("Two input paths ?"); System.out.println(" " + inputPath); System.out.println(" " + s); return; } } } me.process(inputPath); } /** * Process the given file or directory. If a directory, this method will * be called recursively for all child directories and files. * * @param file the file or directory to be processed */ private void process(File file) { if (file.isDirectory()) { for (File child : file.listFiles()) { process(child); } } else { if (file.getName().endsWith(".java")) { try { System.out.println(file.getPath()); processFile(file); } catch (Exception ex) { throw new RuntimeException(ex); } } } } /** * This method performs the task of searching the file for a public class or interface * and its associated javadoc comment block. If found, the comment block is searched * for a source tag which, if absent, will be generated and inserted into the file. * * @param file file to process * @return true if the source tag was inserted into the file; false otherwise * * @throws FileNotFoundException * @throws IOException */ private boolean processFile(File file) throws FileNotFoundException, IOException { Matcher matcher = null; String sourceTagText; /* * Find the svn repo path: trunk, tags or branches */ matcher = findSVNLine.matcher(file.getAbsolutePath()); if (matcher.matches()) { int pos = matcher.start(1); String repoURL = Source.SVN_REPO_URL; StringBuilder sb = new StringBuilder(" * @source "); if (optionSVNDelims) { sb.append("$URL: "); } sb.append(Source.SVN_REPO_URL); sb.append(file.getAbsolutePath().substring(pos)); if (optionSVNDelims) { sb.append(" $"); } sourceTagText = sb.toString(); } else { // don't process this file System.out.println(" --- skipped this file"); System.out.println(); return false; } BufferedReader reader = new BufferedReader(new FileReader(file)); List<String> buffer = new ArrayList<String>(); String line; while ((line = reader.readLine()) != null) { buffer.add(line); } reader.close(); /* * Search the buffer for class header docs and, within that, * the @source tag */ boolean inJavadocBlock = false; boolean inCommentBlock = false; boolean unknownPrecedingContent = false; boolean classFound = false; int javadocStartLine = -1; int javadocEndLine = -1; int sourceTagLine = -1; for (int lineNo = 0; sourceTagLine < 0 && lineNo < buffer.size(); lineNo++) { String text = buffer.get(lineNo); if (inJavadocBlock || inCommentBlock) { matcher = findCommentEnd.matcher(text); if (matcher.find()) { if (inJavadocBlock) { inJavadocBlock = false; javadocEndLine = lineNo; } else if (inCommentBlock) { inCommentBlock = false; } else { System.out.println(" *** Mis-placed end marker for comment block " + "- skipping this file ***"); System.out.println(); return false; } } } else if (findJavadocStart.matcher(text).find()) { inJavadocBlock = true; unknownPrecedingContent = false; javadocStartLine = lineNo; } else if (findCommentStart.matcher(text).find()) { inCommentBlock = true; // Guard against nested or following classes and mention of classes in // comment blocks } else if (!inJavadocBlock && !inCommentBlock && !classFound) { if (optionAnyClass) { matcher = findClass.matcher(text); } else { matcher = findPublicClass.matcher(text); } if (matcher.find()) { classFound = true; /* * If no javadoc comment block preceded the class header * there is nothing to do */ if (javadocStartLine < 0) { System.out.println(" *** No class javadocs - skipping file ***"); System.out.println(); return false; } /* If there were any non-blank lines between the comment and * the class header we will act safely and not modify the file */ if (unknownPrecedingContent) { System.out.println(" *** Javadocs do not directly precede class" + " - skipping file ***"); System.out.println(); return false; } /* * Check if the source tag already exists. If it does, and * the replace tag option is false, skip this file. */ for (int blockLineNo = javadocStartLine; blockLineNo <= javadocEndLine; blockLineNo++) { String commentText = buffer.get(blockLineNo); matcher = findSourceTag.matcher(commentText); if (matcher.find()) { /* * Check that those pesky Eclipse users haven't * split the source tag across multiple lines with * their auto-format thing. */ matcher = findCompleteSourceTag.matcher(commentText); if (!matcher.find()) { if (optionFixBreaks) { matcher = findCompletePath.matcher(buffer.get(blockLineNo + 1)); if (matcher.find()) { buffer.remove(blockLineNo + 1); buffer.remove(blockLineNo); sourceTagLine = blockLineNo; if (!optionReplace) { // Make the new tag text the old lines joined together. // String http = matcher.group(); int start = http.indexOf("$URL"); if (start < 0) start = http.indexOf("http"); http = http.substring(start, http.length()); sourceTagText = commentText + http; } System.out.println(" *** Fixing broken source tag ***"); break; } } else { // Just report the broken tag and skip this file System.out.println(" *** Incomplete source tag detected" + "- skipping this file ***"); System.out.println(); return false; } } if (optionReplace) { sourceTagLine = blockLineNo; // delete the original tag from the buffer buffer.remove(blockLineNo); break; } else { return false; } } } if (sourceTagLine < 0) { /* * Check if the version tag exists. If it does we * will place the source tag on the line before it */ for (int i = javadocStartLine; i <= javadocEndLine; i++) { matcher = findVersionTag.matcher(buffer.get(i)); if (matcher.find()) { sourceTagLine = i; break; } } } if (sourceTagLine < 0) { sourceTagLine = javadocEndLine; } } else { /* * Not a comment line or the class header. Check if it is * a non-emptyLine */ if (text.trim().length() > 0) { // Annotations are OK matcher = findAnnotation.matcher(text); if (!matcher.find()) { unknownPrecedingContent = true; } } } } } /* * If the search was successful write the results to file */ if (sourceTagLine > 0) { return writeFile(file, buffer, sourceTagLine, sourceTagText); } return false; } /** * Writes the file with a newly generated source tag in the class header * javadocs * * @param file the file to write * @param buffer file contents * @param sourceTagLine line number for the new source tag * @param sourceTag text for the new source tag * * @return always returns true * * @throws IOException */ private boolean writeFile(File file, List<String> buffer, int sourceTagLine, String sourceTag) throws IOException { FileWriter writer = new FileWriter(file); for (int i = 0; i < buffer.size(); i++) { if (i == sourceTagLine) { writer.write(" *" + lineSeparator); writer.write(sourceTag); writer.write(lineSeparator); } writer.write(buffer.get(i)); writer.write(lineSeparator); } writer.close(); return true; } }