/*
* Copyright 2014 the original author or authors.
*
* 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 org.springframework.xd.documentation;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.xd.dirt.module.ModuleRegistry;
import org.springframework.xd.dirt.module.ResourceModuleRegistry;
import org.springframework.xd.module.ModuleDefinition;
import org.springframework.xd.module.ModuleType;
import org.springframework.xd.module.SimpleModuleDefinition;
import org.springframework.xd.module.options.DefaultModuleOptionsMetadataResolver;
import org.springframework.xd.module.options.ModuleOption;
import org.springframework.xd.module.options.ModuleOptionsMetadata;
import org.springframework.xd.module.options.ModuleOptionsMetadataResolver;
import org.springframework.xd.module.options.ModuleUtils;
import org.springframework.xd.module.options.spi.ModulePlaceholders;
/**
* A class that generates asciidoc snippets for each module's options.
*
* <p>For each file passed as an argument, will replace parts of the file (inplace) in between {@code //^<type>.<name>}
* and {@code //$<type>.<name>} with a generated snippet documenting options. Those start and end fences are copied as-is,
* so that a subsequent run regenerates uptodate doco.
* </p>
*
* @author Eric Bottard
*/
public class ModuleOptionsReferenceDoc {
/**
* Matches "//^<type>.<name>" exactly.
*/
private static final Pattern FENCE_START_REGEX = Pattern.compile("^//\\^([^.]+)\\.([^.]+)$");
private ModuleRegistry moduleRegistry = new ResourceModuleRegistry("file:./modules");
private ModuleOptionsMetadataResolver moduleOptionsMetadataResolver = new DefaultModuleOptionsMetadataResolver();
private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
public static void main(String... paths) throws IOException {
ModuleOptionsReferenceDoc runner = new ModuleOptionsReferenceDoc();
for (String path : paths) {
runner.updateSingleFile(path);
}
}
private void updateSingleFile(String path) throws IOException {
File originalFile = new File(path);
Assert.isTrue(originalFile.exists() && !originalFile.isDirectory(),
String.format("'%s' does not exist or points to a directory", originalFile.getAbsolutePath()));
File backup = new File(originalFile.getAbsolutePath() + ".backup");
originalFile.renameTo(backup);
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(backup), "UTF-8"));
PrintStream out = new PrintStream(new FileOutputStream(originalFile), false, "UTF-8");
ModuleType type = null;
String name = null;
int openingLineNumber = 0;
int ln = 1;
for (String line = reader.readLine(); line != null; line = reader.readLine(), ln++) {
Matcher startMatcher = FENCE_START_REGEX.matcher(line);
if (startMatcher.matches()) {
checkPreviousTagHasBeenClosed(originalFile, backup, out, type, name, openingLineNumber);
type = ModuleType.valueOf(startMatcher.group(1));
name = startMatcher.group(2);
openingLineNumber = ln;
out.println(line);
}
else if (type != null && line.equals(String.format("//$%s.%s", type, name))) {
generateWarning(out, name, type);
generateAsciidoc(out, name, type);
type = null;
name = null;
out.println(line);
}
else if (type == null) {
out.println(line);
}
}
checkPreviousTagHasBeenClosed(originalFile, backup, out, type, name, openingLineNumber);
out.close();
reader.close();
backup.delete();
}
private void checkPreviousTagHasBeenClosed(File originalFile, File backup, PrintStream out, ModuleType type,
String name, int openingLineNumber) {
if (type != null) {
out.close();
originalFile.delete();
backup.renameTo(originalFile);
throw new IllegalStateException(String.format(
"In %s, found '//^%s.%s' @line %d with no matching '//$%2$s.%3$s'",
originalFile.getAbsolutePath(), type, name, openingLineNumber));
}
}
private void generateWarning(PrintStream out, String name, ModuleType type) {
out.format("// DO NOT MODIFY THE LINES BELOW UNTIL THE CLOSING '//$%s.%s' TAG%n", type, name);
out.format("// THIS SNIPPET HAS BEEN GENERATED BY %s AND MANUAL EDITS WILL BE LOST%n",
ModuleOptionsReferenceDoc.class.getSimpleName());
}
private void generateAsciidoc(PrintStream out, String name, ModuleType type)
throws IOException {
ModuleDefinition def = moduleRegistry.findDefinition(name, type);
ModuleOptionsMetadata moduleOptionsMetadata = moduleOptionsMetadataResolver.resolve(def);
Resource moduleLoc = resourcePatternResolver.getResource(((SimpleModuleDefinition) def).getLocation());
ClassLoader moduleClassLoader = ModuleUtils.createModuleDiscoveryClassLoader(moduleLoc, ModuleOptionsReferenceDoc.class.getClassLoader());
if (!moduleOptionsMetadata.iterator().hasNext()) {
out.format("The **%s** %s has no particular option (in addition to options shared by all modules)%n%n",
pt(def.getName()), pt(def.getType()));
return;
}
out.format("The **%s** %s has the following options:%n%n", pt(def.getName()), pt(def.getType()));
List<ModuleOption> options = new ArrayList<ModuleOption>();
for (ModuleOption mo : moduleOptionsMetadata) {
options.add(mo);
}
Collections.sort(options, new Comparator<ModuleOption>() {
@Override
public int compare(ModuleOption o1, ModuleOption o2) {
return o1.getName().compareTo(o2.getName());
}
});
for (ModuleOption mo : options) {
String prettyDefault = prettifyDefaultValue(mo);
String maybeEnumHint = generateEnumValues(mo, moduleClassLoader);
out.format("%s:: %s *(%s, %s%s)*%n", pt(mo.getName()), pt(mo.getDescription()),
pt(shortClassName(mo.getType())),
prettyDefault, maybeEnumHint);
}
}
private String shortClassName(String fqName) {
int lastDot = fqName.lastIndexOf('.');
return lastDot >= 0 ? fqName.substring(lastDot + 1) : fqName;
}
/**
* When the type of an option is an enum, document all possible values
*/
private String generateEnumValues(ModuleOption mo, ClassLoader moduleClassLoader) {
// Attempt to convert back to com.acme.Foo$Bar form
String canonical = mo.getType();
String system = canonical.replaceAll("(.*\\p{Upper}[^\\.]*)\\.(\\p{Upper}.*)", "$1\\$$2");
Class<?> clazz = null;
try {
clazz = Class.forName(system, false, moduleClassLoader);
}
catch (ClassNotFoundException e) {
return "";
}
if (Enum.class.isAssignableFrom(clazz)) {
String values = StringUtils.arrayToCommaDelimitedString(clazz.getEnumConstants());
return String.format(", possible values: `%s`", values);
}
else
return "";
}
private String prettifyDefaultValue(ModuleOption mo) {
if (mo.getDefaultValue() == null) {
return "no default";
}
String result = stringify(mo.getDefaultValue());
result = result.replace(ModulePlaceholders.XD_STREAM_NAME, "<stream name>");
result = result.replace(ModulePlaceholders.XD_JOB_NAME, "<job name>");
return "default: `" + result + "`";
}
private String stringify(Object element) {
Class<?> clazz = element.getClass();
if (clazz == byte[].class) {
return Arrays.toString((byte[]) element);
}
else if (clazz == short[].class) {
return Arrays.toString((short[]) element);
}
else if (clazz == int[].class) {
return Arrays.toString((int[]) element);
}
else if (clazz == long[].class) {
return Arrays.toString((long[]) element);
}
else if (clazz == char[].class) {
return Arrays.toString((char[]) element);
}
else if (clazz == float[].class) {
return Arrays.toString((float[]) element);
}
else if (clazz == double[].class) {
return Arrays.toString((double[]) element);
}
else if (clazz == boolean[].class) {
return Arrays.toString((boolean[]) element);
}
else if (element instanceof Object[]) {
return Arrays.deepToString((Object[]) element);
}
else {
return element.toString();
}
}
/**
* Return an asciidoc passthrough version of some text, in case the original text contains characters
* that would be (mis)interpreted by asciidoc.
*/
private String pt(Object original) {
return "$$" + original + "$$";
}
}