blob: 6f8d30960b0b1cbeccddfdecb025fbaa07b3785f [file] [log] [blame]
/*
* 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.
*/
package org.apache.sis.internal.referencing.provider;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.lang.reflect.Field;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import javax.measure.Unit;
import org.opengis.util.GenericName;
import org.opengis.metadata.Identifier;
import org.apache.sis.parameter.DefaultParameterDescriptor;
import org.apache.sis.test.ProjectDirectories;
import org.apache.sis.internal.jdk9.JDK9;
import org.apache.sis.measure.Angle;
import org.apache.sis.measure.Latitude;
import org.apache.sis.measure.Longitude;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.StringBuilders;
import org.apache.sis.measure.Range;
import static org.junit.Assert.*;
/**
* Inserts comments with parameter names in the javadoc of parameters.
* This class needs to be run explicitly; it is not part of JUnit tests.
* After execution, files in the provider packages may be overwritten.
* Developer should execute {@code "git diff"} and inspect the changes.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
public final class ParameterNameTableGenerator extends SimpleFileVisitor<Path> {
/**
* Value as the kind of object expected in {@link DefaultParameterDescriptor}.
*/
private static final Double POSITIVE_ZERO = +0d,
NEGATIVE_ZERO = -0d,
MIN_LONGITUDE = Longitude.MIN_VALUE,
MAX_LONGITUDE = Longitude.MAX_VALUE,
MIN_LATITUDE = Latitude.MIN_VALUE,
MAX_LATITUDE = Latitude.MAX_VALUE;
/**
* The directory of Java source code to scan.
*/
private final Path directory;
/**
* Pattern of the lines to search.
*/
private final Pattern toSearch;
/**
* A temporary buffer for creating lines of comment.
*/
private final StringBuilder buffer;
/**
* All lines in the file being processed.
*/
private List<String> lines;
/**
* For {@link #main(String[])} only.
*/
private ParameterNameTableGenerator() {
directory = new ProjectDirectories(getClass()).getSourcesPackageDirectory("core/sis-referencing");
toSearch = Pattern.compile(".*\\s+static\\s+.*ParameterDescriptor<\\w+>\\s*(\\w+)\\s*[=;].*");
buffer = new StringBuilder();
}
/**
* Launches the insertion of comment lines.
*
* @param args ignored.
* @throws IOException if an error occurred while reading or writing a file.
*/
public static void main(final String[] args) throws IOException {
final ParameterNameTableGenerator cg = new ParameterNameTableGenerator();
Files.walkFileTree(cg.directory, cg);
}
/**
* Invoked before to enter in a sub-directory. This implementation skips all sub-directories.
*
* @param dir the directory in which to enter.
* @param attrs ignored.
* @return flag instructing whether to scan that directory or not.
*/
@Override
public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) {
return dir.equals(directory) ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
}
/**
* Invoked for each file in the scanned directory. If the file is a Java file,
* searches for {@code ParameterDescriptor} declarations. Otherwise ignore.
*
* @param file the file.
* @param attrs ignored.
* @return {@link FileVisitResult#CONTINUE}.
* @throws IOException if an error occurred while reading or writing the given file.
*/
@Override
public FileVisitResult visitFile​(final Path file, final BasicFileAttributes attrs) throws IOException {
final String name = file.getFileName().toString();
if (name.endsWith(".java")) {
addCommentsToJavaFile(file);
}
return super.visitFile(file, attrs);
}
/**
* Returns the class for the given Java source file.
*/
private Class<?> getClass(final Path file) throws ClassNotFoundException {
String name = file.getFileName().toString();
name = name.substring(0, name.lastIndexOf('.')); // Remove the ".java" suffix.
name = JDK9.getPackageName(getClass()) + '.' + name;
return Class.forName(name);
}
/**
* Finds the parameter descriptors in the given file, and add parameter names in comments.
*/
private void addCommentsToJavaFile(final Path file) throws IOException {
Class<?> classe = null;
final Matcher matcher = toSearch.matcher("");
lines = Files.readAllLines(file);
for (int i=lines.size(); --i >= 0;) {
final String line = lines.get(i);
if (matcher.reset(line).matches()) {
final String fieldName = matcher.group(1);
final DefaultParameterDescriptor<?> descriptor;
try {
if (classe == null) {
classe = getClass(file);
}
final Field field = classe.getDeclaredField(fieldName);
field.setAccessible(true);
descriptor = (DefaultParameterDescriptor<?>) field.get(null);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
/*
* Find the line where to insert comments. We detect if the comment to insert already exists
* by looking for this class name (expected in a HTML comment) before the start of comments.
* If we find that line, then we delete everything between it and the end of comments before
* to regenerate them. Developer can execute "git diff" on the command line for checking if
* any lines changed as a result.
*/
int insertAt = i;
String previous;
do {
previous = lines.get(--insertAt).trim();
if (!previous.startsWith("*") && !previous.startsWith("@")) {
fail("Unexpected content in " + file.getFileName() + " at line " + insertAt);
}
} while (!previous.equals("*/"));
previous = lines.get(insertAt);
buffer.setLength(0);
buffer.append(previous, 0, previous.indexOf('*') + 1);
for (int check = insertAt;;) {
previous = lines.get(--check).trim();
if (previous.equals("/**")) break;
if (previous.contains("ParameterNameTableGenerator")) {
lines.subList(check, insertAt).clear();
insertAt = check;
break;
}
}
/*
* Format a HTML table in the comment with the name and aliases of each parameter.
*/
write(insertAt++, "<!-- Generated by ParameterNameTableGenerator -->");
write(insertAt++, "<table class=\"sis\">");
write(insertAt++, " <caption>Parameter names</caption>");
write(insertAt++, descriptor.getName());
for (final GenericName alias : descriptor.getAlias()) {
write(insertAt++, (Identifier) alias);
}
write(insertAt++, "</table>");
/*
* Format other information: default value, value domain, whether the value is mandatory, etc.
* Default value of zero are omitted (i.e. unless otherwise specified, default values are zero
* in the tables that we format). Range of values of [-90 … 90]° for latitude or [-180 … 180]°
* for longitude are also omitted. In other words, we report only "unusual" things in the notes.
*/
Object defaultValue = descriptor.getDefaultValue();
Range<?> valueDomain = descriptor.getValueDomain();
boolean isOptional = descriptor.getMinimumOccurs() == 0;
boolean noDefault = (defaultValue == null);
Object minValue = null;
Object maxValue = null;
if (valueDomain != null) {
minValue = valueDomain.getMinValue();
maxValue = valueDomain.getMaxValue();
final boolean inclusive = valueDomain.isMinIncluded() && valueDomain.isMaxIncluded();
if (fieldName.contains("LATITUDE") || fieldName.contains("PARALLEL")) {
if (inclusive && MIN_LATITUDE.equals(minValue) && MAX_LATITUDE.equals(maxValue)) {
valueDomain = null;
}
} else if (fieldName.contains("LONGITUDE") || fieldName.contains("MERIDIAN")) {
if (inclusive && MIN_LONGITUDE.equals(minValue) && MAX_LONGITUDE.equals(maxValue)) {
valueDomain = null;
}
} else if (minValue == null && maxValue == null) {
valueDomain = null;
}
}
if (POSITIVE_ZERO.equals(defaultValue)) {
defaultValue = null;
}
if (defaultValue != null || valueDomain != null || isOptional || noDefault) {
write(insertAt++, "<b>Notes:</b>");
write(insertAt++, "<ul>");
if (valueDomain != null) {
final int p = buffer.length();
buffer.append(" <li>");
if ((minValue != null && minValue.equals(maxValue)) ||
(NEGATIVE_ZERO.equals(minValue) && POSITIVE_ZERO.equals(maxValue)))
{
buffer.append("Value restricted to ").append(maxValue);
StringBuilders.trimFractionalPart(buffer);
} else {
buffer.append("Value domain: ").append(valueDomain);
}
lines.add(insertAt++, buffer.append("</li>").toString());
buffer.setLength(p);
}
if (defaultValue != null) {
final int p = buffer.length();
final boolean isText = !(defaultValue instanceof Number || defaultValue instanceof Angle);
buffer.append(" <li>").append("Default value: ");
if (isText) buffer.append("{@code ");
buffer.append(defaultValue);
if (isText) buffer.append('}');
StringBuilders.trimFractionalPart(buffer);
final Unit<?> unit = descriptor.getUnit();
if (unit != null) {
final String symbol = unit.getSymbol();
if (!symbol.isEmpty()) {
if (Character.isLetterOrDigit(symbol.charAt(0))) {
buffer.append(' ');
}
buffer.append(symbol);
}
}
lines.add(insertAt++, buffer.append("</li>").toString());
buffer.setLength(p);
} else {
write(insertAt++, " <li>No default value</li>");
}
if (isOptional) {
write(insertAt++, " <li>Optional</li>");
}
write(insertAt++, "</ul>");
}
}
}
/*
* If at least one table has been formatted, rewrite the file.
*/
if (classe != null) {
Files.write(file, lines);
}
lines = null;
}
/**
* Appends the given line at the given position. This method writes the margin
* (typically spaces followed by {@code '*'} and a single space) before the line.
*/
private void write(final int insertAt, final String line) {
final int p = buffer.length();
if (!line.isEmpty()) {
buffer.append(' ').append(line);
}
lines.add(insertAt, buffer.toString());
buffer.setLength(p);
}
/**
* Appends the given authority and name at the given position. This method writes the margin
* (typically spaces followed by {@code '*'} and a single space) before the line.
*/
private void write(final int insertAt, final Identifier id) {
final int p = buffer.length();
final String authority = id.getCodeSpace();
buffer.append(" <tr><td> ").append(authority).append(':')
.append(CharSequences.spaces(8 - authority.length()))
.append("</td><td> ").append(id.getCode()).append(" </td></tr>");
lines.add(insertAt, buffer.toString());
buffer.setLength(p);
}
}