This is an automated email from the ASF dual-hosted git repository. ddekany pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/freemarker-docgen.git
commit 793e8da7684405fa901086b3dad3040fe094d231 Author: ddekany <[email protected]> AuthorDate: Mon Jan 25 00:58:58 2021 +0100 - Added getCustomVariable(name) and concat(...) function to CJSON - Generalized up [docgen.insertXxx ...] tag parsing a bit - Added unfinished implementation of [docgen.insertOutput ...] directive. This will be used to simplify inserting the output of "programs" into the documentation. --- .../docgen/core/BashCommandLineArgsParser.java | 98 +++++++ .../freemarker/docgen/core/CJSONInterpreter.java | 17 +- .../core/DocgenSubstitutionTemplateException.java | 37 --- .../PrintTextWithDocgenSubstitutionsDirective.java | 292 +++++++++++++-------- .../java/org/freemarker/docgen/core/Transform.java | 145 +++++++++- .../docgen/core/BashCommandLineArgsParserTest.java | 43 +++ 6 files changed, 473 insertions(+), 159 deletions(-) diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/BashCommandLineArgsParser.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/BashCommandLineArgsParser.java new file mode 100644 index 0000000..c464b34 --- /dev/null +++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/BashCommandLineArgsParser.java @@ -0,0 +1,98 @@ +/* + * 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.freemarker.docgen.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * Splits a bash command call to a list of arguments. Quotation and escaping is resolved in the returned arguments. + */ +public class BashCommandLineArgsParser { + private final String src; + private int pos; + + public BashCommandLineArgsParser(String src) { + this.src = src; + } + + public static List<String> parse(String s) { + return new BashCommandLineArgsParser(s).parse(); + } + + private List<String> parse() { + List<String> args = new ArrayList<>(); + String arg; + while ((arg = skipWSAndFetchArg()) != null) { + args.add(arg); + } + return args; + } + + private String skipWSAndFetchArg() { + skipWS(); + return fetchArg(); + } + + private String fetchArg() { + StringBuilder arg = new StringBuilder(); + int startPos = pos; + char openedQuote = 0; + boolean escaped = false; + while (pos < src.length()) { + char c = src.charAt(pos); + if (escaped) { + if (openedQuote == '"' && !(c == '"' || c == '\\' || c == '$')) { + arg.append('\\'); + } + arg.append(c); + escaped = false; + } else { + if (c == '"' || c == '\'') { + if (openedQuote == 0) { + openedQuote = c; + } else if (openedQuote == c) { + openedQuote = 0; + } else { + arg.append(c); + } + } else if (c == '\\' && openedQuote != '\'') { + escaped = true; + } else if (openedQuote == 0 && isWS(c)) { + break; + } else { + arg.append(c); + } + } + pos++; + } + return startPos != pos ? arg.toString() : null; + } + + private void skipWS() { + while (pos < src.length() && isWS(src.charAt(pos))) { + pos++; + } + } + + private boolean isWS(char c) { + return c == ' ' || c == '\n' || c == '\r' || c == '\t'; + } +} diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java index 543aea6..fc43e10 100644 --- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java +++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/CJSONInterpreter.java @@ -329,22 +329,23 @@ final class CJSONInterpreter { } } + public static Map<String, Object> evalAsMap(File f) + throws EvaluationException, IOException { + return evalAsMap(f, null, false); + } + /** * Same as <code>evalAsMap(textFromUTF8File, null, false, null)</code>. - * The file must use UTF-8 encoding. Initial BOM is allowed. - * @throws IOException + * Loads the file with {@link #loadCJSONFile}. * @see #evalAsMap(String, EvaluationEnvironment, boolean, String) */ - public static Map<String, Object> evalAsMap(File f) + public static Map<String, Object> evalAsMap(File f, EvaluationEnvironment ee, boolean forceStringValues) throws EvaluationException, IOException { String s; - InputStream in = new FileInputStream(f); - try { + try (InputStream in = new FileInputStream(f)) { s = loadCJSONFile(in, f.getAbsolutePath()); - } finally { - in.close(); } - return evalAsMap(s, f.getAbsolutePath()); + return evalAsMap(s, ee, forceStringValues, f.getAbsolutePath()); } /** diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenSubstitutionTemplateException.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenSubstitutionTemplateException.java deleted file mode 100644 index 4c1e805..0000000 --- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/DocgenSubstitutionTemplateException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.freemarker.docgen.core; - -import freemarker.core.Environment; -import freemarker.template.TemplateException; - -/** - * Exception thrown by docgen tag-s that are inside the XML text. As such, it's treated as the mistake of the document - * author (as opposed to an internal error). - */ -final class DocgenSubstitutionTemplateException extends TemplateException { - public DocgenSubstitutionTemplateException(String description, Environment env) { - super(description, env); - } - - public DocgenSubstitutionTemplateException(String description, Exception cause, Environment env) { - super(description, cause, env); - } -} diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java index c8110f0..55adcbf 100644 --- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java +++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PrintTextWithDocgenSubstitutionsDirective.java @@ -21,19 +21,16 @@ package org.freemarker.docgen.core; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; import java.io.Writer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -65,7 +62,9 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect private static final String PARAM_TEXT = "text"; private static final String DOCGEN_TAG_START = "[docgen"; private static final String DOCGEN_TAG_END = "]"; + private static final String DOCGEN_END_TAG_START = "[/docgen"; private static final String INSERT_FILE = "insertFile"; + private static final String INSERT_OUTPUT = "insertOutput"; private final Transform transform; @@ -144,40 +143,13 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect insertCustomVariable(customVarName); } else if (INSERT_FILE.equals(subvarName)) { - skipWS(); - String pathArg = fetchRequiredString(); - String charsetArg = null; - String fromArg = null; - String toArg = null; - String toIfPresentArg = null; - Set<String> paramNamesSeen = new HashSet<>(); - while (skipWS()) { - String paramName = fetchOptionalVariableName(); - skipRequiredToken("="); - String paramValue = StringEscapeUtils.unescapeXml(fetchRequiredString()); - if (!paramNamesSeen.add(paramName)) { - throw new TemplateException( - "Duplicate " + StringUtil.jQuote(INSERT_FILE) - + " parameter " + StringUtil.jQuote(paramName) + ".", env); - } - if (paramName.equals("charset")) { - charsetArg = paramValue; - } else if (paramName.equals("from")) { - fromArg = paramValue; - } else if (paramName.equals("to")) { - toArg = paramValue; - } else if (paramName.equals("toIfPresent")) { - toIfPresentArg = paramValue; - } else { - throw new TemplateException( - "Unsupported " + StringUtil.jQuote(INSERT_FILE) - + " parameter " + StringUtil.jQuote(paramName) + ".", env); - } - } - skipRequiredToken(DOCGEN_TAG_END); + InsertDirectiveArgs args = fetchInsertDirectiveArgs(subvarName, true, true, false); lastUnprintedIdx = cursor; - - insertFile(pathArg, charsetArg, fromArg, toArg, toIfPresentArg); + insertFile(args); + } else if (INSERT_OUTPUT.equals(subvarName)) { + InsertDirectiveArgs args = fetchInsertDirectiveArgs(subvarName, false, false, true); + lastUnprintedIdx = cursor; + insertOutput(args); } else { throw new TemplateException( "Unsupported docgen subvariable " + StringUtil.jQuote(subvarName) + ".", env); @@ -239,11 +211,9 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect } } - private void insertFile(String pathArg, String charsetArg, String fromArg, - String toArg, String toIfPresentArg) - throws TemplateException, IOException { - int slashIndex = pathArg.indexOf("/"); - String symbolicNameStep = slashIndex != -1 ? pathArg.substring(0, slashIndex) : pathArg; + private void insertFile(InsertDirectiveArgs args) throws TemplateException, IOException { + int slashIndex = args.path.indexOf("/"); + String symbolicNameStep = slashIndex != -1 ? args.path.substring(0, slashIndex) : args.path; if (!symbolicNameStep.startsWith("@") || symbolicNameStep.length() < 2) { throw newErrorInDocgenTag("Path argument must start with @<symbolicName>/, " + " where <symbolicName> is in " + transform.getInsertableFiles().keySet() + "."); @@ -257,7 +227,7 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect } symbolicNamePath = symbolicNamePath.toAbsolutePath().normalize(); Path resolvedFilePath = slashIndex != -1 - ? symbolicNamePath.resolve(pathArg.substring(slashIndex + 1)) + ? symbolicNamePath.resolve(args.path.substring(slashIndex + 1)) : symbolicNamePath; resolvedFilePath = resolvedFilePath.normalize(); if (!resolvedFilePath.startsWith(symbolicNamePath)) { @@ -270,11 +240,11 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect } Charset charset; - if (charsetArg != null) { + if (args.charset != null) { try { - charset = Charset.forName(charsetArg); + charset = Charset.forName(args.charset); } catch (UnsupportedCharsetException e) { - throw newErrorInDocgenTag("Unsupported charset: " + charsetArg); + throw newErrorInDocgenTag("Unsupported charset: " + args.charset); } } else { charset = StandardCharsets.UTF_8; @@ -287,70 +257,29 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect fileContent = removeFTLCopyrightComment(fileContent); } - if (fromArg != null) { - boolean optional; - String fromArgCleaned; - if (fromArg.startsWith("?")) { - optional = true; - fromArgCleaned = fromArg.substring(1); - } else { - optional = false; - fromArgCleaned = fromArg; - } - Pattern from; - try { - from = Pattern.compile(fromArgCleaned, Pattern.MULTILINE); - } catch (PatternSyntaxException e) { - throw newErrorInDocgenTag("Invalid regular expression: " + fromArgCleaned); - } - Matcher matcher = from.matcher(fileContent); + if (args.from != null) { + Matcher matcher = args.from.matcher(fileContent); if (matcher.find()) { String remaining = fileContent.substring(matcher.start()); fileContent = "[\u2026]" + (remaining.startsWith("\n") || remaining.startsWith("\r") ? "" : "\n") + remaining; - } else { - if (!optional) { - throw newErrorInDocgenTag( - "Regular expression has no match in the file content: " + fromArg); - } - } - } - - String toStr; - boolean toPresenceOptional; - if (toArg != null) { - if (toIfPresentArg != null) { + } else if (!args.fromOptional) { throw newErrorInDocgenTag( - "Can't use both \"to\" and \"toIfPresent\" argument."); + "\"from\" regular expression has no match in the file content: " + args.from); } - toStr = toArg; - toPresenceOptional = false; - } else if (toIfPresentArg != null) { - toStr = toIfPresentArg; - toPresenceOptional = true; - } else { - toStr = null; - toPresenceOptional = false; } - if (toStr != null) { - Pattern to; - try { - to = Pattern.compile(toStr, Pattern.MULTILINE); - } catch (PatternSyntaxException e) { - throw newErrorInDocgenTag("Invalid regular expression: " + toStr); - } - Matcher matcher = to.matcher(fileContent); + + if (args.to != null) { + Matcher matcher = args.to.matcher(fileContent); if (matcher.find()) { String remaining = fileContent.substring(0, matcher.start()); fileContent = remaining + (remaining.endsWith("\n") || remaining.endsWith("\r") ? "" : "\n") + "[\u2026]"; - } else { - if (!toPresenceOptional) { - throw newErrorInDocgenTag( - "Regular expression has no match in the file content: " + toStr); - } + } else if (!args.toOptional) { + throw newErrorInDocgenTag( + "\"to\" regular expression has no match in the file content: " + args.to); } } @@ -358,6 +287,28 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect } } + private void insertOutput(InsertDirectiveArgs args) throws TemplateException, IOException { + List<String> splitCmdLine = BashCommandLineArgsParser.parse(args.body); + if (splitCmdLine.isEmpty()) { + throw newErrorInDocgenTag("Command to execute was empty"); + } + String cmdKey = splitCmdLine.get(0); + List<String> cmdArgs = splitCmdLine.subList(1, splitCmdLine.size()); + Map<String, Transform.InsertableOutputCommandProperties> cmdPropsMap = + transform.getInsertableOutputCommands(); + Transform.InsertableOutputCommandProperties cmdProps = cmdPropsMap.get(cmdKey); + if (cmdProps == null) { + throw newErrorInDocgenTag( + "The " + Transform.SETTING_INSERTABLE_OUTPUT_COMMANDS + + " configuration setting doesn't have entry with key " + StringUtil.jQuote(cmdKey) + + ". " + + (cmdPropsMap.isEmpty() + ? "That setting is empty." + : "It has these keys: " + String.join(", ", cmdPropsMap.keySet()))); + } + HTMLOutputFormat.INSTANCE.output("!!T\n" + cmdProps + "\n" + cmdArgs, out); + } + private TemplateException newFormattingFailedException(String customVarName, TemplateValueFormatException e) { return new TemplateException( "Formatting failed for Docgen custom variable " @@ -365,8 +316,8 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect e, env); } - private int findNextDocgenTagStart(int lastUnprintedIdx) { - int startIdx = text.indexOf(DOCGEN_TAG_START, lastUnprintedIdx); + private int findNextDocgenTagStart(int fromIndex) { + int startIdx = text.indexOf(DOCGEN_TAG_START, fromIndex); if (startIdx == -1) { return -1; } @@ -378,6 +329,25 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect return -1; } + private int findNextDocgenEndTag(int fromIndex) { + int startIdx = text.indexOf(DOCGEN_END_TAG_START, fromIndex); + if (startIdx == -1) { + return -1; + } + int afterTagStartIdx = startIdx + DOCGEN_END_TAG_START.length(); + if (afterTagStartIdx < text.length() + && !Character.isJavaIdentifierPart(text.charAt(afterTagStartIdx))) { + return startIdx; + } + return -1; + } + + private void skipRequiredWS() throws DocgenTagException { + if (!skipWS()) { + throw newUnexpectedTokenException("whitespace", env); + } + } + private boolean skipWS() { boolean found = false; while (cursor < text.length()) { @@ -454,36 +424,139 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect int stringStartIdx = cursor; while (cursor < text.length() && charAt(cursor) != quoteChar) { if (!rawString && charAt(cursor) == '\\') { - throw new DocgenSubstitutionTemplateException( + throw new DocgenTagException( "Backslash is currently not supported in string literal in Docgen tags, " + "except in raw strings (like r\"regular\\s+expression\").", env); } cursor++; } if (charAt(cursor) != quoteChar) { - throw new DocgenSubstitutionTemplateException("Unclosed string literal in a Docgen tag.", env); + throw new DocgenTagException("Unclosed string literal in a Docgen tag.", env); } String result = text.substring(stringStartIdx, cursor); cursor++; return result; } + private boolean fetchRequiredBoolean() throws TemplateException { + Boolean result = fetchOptionalBoolean(); + if (result == null) { + throw newUnexpectedTokenException("boolean", env); + } + return result; + } + + private Boolean fetchOptionalBoolean() throws DocgenTagException { + String name = fetchOptionalVariableName(); + if (name == null) { + return null; + } + if (name.equals("true")) { + return true; + } else if (name.equals("false")) { + return false; + } else { + throw new DocgenTagException("true or false", env); + } + } + + private char charAt(int index) { return index < text.length() ? text.charAt(index) : 0; } - private TemplateException newUnexpectedTokenException(String expectedTokenDesc, Environment env) { - return new DocgenSubstitutionTemplateException( + private DocgenTagException newUnexpectedTokenException(String expectedTokenDesc, Environment env) { + return new DocgenTagException( "Expected " + expectedTokenDesc + " after this: " + text.substring(lastDocgenTagStart, cursor), env); } private TemplateException newErrorInDocgenTag(String errorDetail) { - return new DocgenSubstitutionTemplateException( + return new DocgenTagException( "\nError in docgen tag: " + text.substring(lastDocgenTagStart, cursor) + "\n" + errorDetail, env); } + + private InsertDirectiveArgs fetchInsertDirectiveArgs( + String subvarName, boolean hasPath, boolean allowCharsetArg, boolean hasBodyArg) throws + TemplateException { + InsertDirectiveArgs args = new InsertDirectiveArgs(); + args.toOptional = true; + + if (hasPath) { + skipWS(); + args.path = fetchRequiredString(); + } + + Set<String> paramNamesSeen = new HashSet<>(); + String paramName; + while (skipWS() && (paramName = fetchOptionalVariableName()) != null) { + skipRequiredToken("="); + if (!paramNamesSeen.add(paramName)) { + throw new DocgenTagException( + "Duplicate docgen." + subvarName + " parameter " + StringUtil.jQuote(paramName) + ".", + env); + } + if (allowCharsetArg && paramName.equals("charset")) { + args.charset = StringEscapeUtils.unescapeXml(fetchRequiredString()); + } else if (paramName.equals("from")) { + args.from = parseRegularExpressionParam(paramName, StringEscapeUtils.unescapeXml(fetchRequiredString())); + } else if (paramName.equals("to")) { + args.to = parseRegularExpressionParam(paramName, StringEscapeUtils.unescapeXml(fetchRequiredString())); + } else if (paramName.equals("fromOptional")) { + args.fromOptional = fetchRequiredBoolean(); + } else if (paramName.equals("toOptional")) { + args.toOptional = fetchRequiredBoolean(); + } else { + throw new DocgenTagException( + "Unsupported docgen." + subvarName + " parameter " + StringUtil.jQuote(paramName) + ".", + env); + } + } + + skipRequiredToken(DOCGEN_TAG_END); + int indexAfterStartTag = cursor; + + if (hasBodyArg) { + int endTagIndex = findNextDocgenEndTag(cursor); + if (endTagIndex == -1) { + throw new DocgenTagException( + "Missing docgen end-tag after " + DOCGEN_TAG_START + "." + subvarName + " ...]", env); + } + lastDocgenTagStart = endTagIndex; + + args.body = StringEscapeUtils.unescapeXml(text.substring(indexAfterStartTag, endTagIndex)); + + cursor = endTagIndex + DOCGEN_END_TAG_START.length(); + skipRequiredToken("."); + String endSubvarName = fetchRequiredVariableName(); + if (!endSubvarName.equals(subvarName)) { + throw new DocgenTagException( + "End-tag " + DOCGEN_END_TAG_START + "." + endSubvarName + "] doesn't match " + + DOCGEN_TAG_START + "." + subvarName + " ...] tag.", env); + } + skipRequiredToken("]"); + } + + args.indexAfterDirective = cursor; + + return args; + } + + private Pattern parseRegularExpressionParam(String paramName, String paramValue) throws TemplateException { + Objects.requireNonNull(paramName); + Objects.requireNonNull(paramValue); + Pattern parsedParamValue; + try { + parsedParamValue = Pattern.compile(paramValue, Pattern.MULTILINE); + } catch (PatternSyntaxException e) { + throw newErrorInDocgenTag("Invalid regular expression for parameter \"" + + paramName + "\": " + paramValue); + } + return parsedParamValue; + } + } public static String removeFTLCopyrightComment(String ftl) { @@ -534,4 +607,15 @@ public class PrintTextWithDocgenSubstitutionsDirective implements TemplateDirect return ftl.substring(0, commentFirstIdx) + ftl.substring(commentLastIdx + afterCommentNLChars + 1); } + static class InsertDirectiveArgs { + private String path; + private String charset; + private Pattern from; + private boolean fromOptional; + private Pattern to; + private boolean toOptional; + private String body; + private int indexAfterDirective; + } + } diff --git a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java index 37da9ab..20eaead 100644 --- a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java +++ b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java @@ -45,7 +45,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.Set; import java.util.SortedMap; import java.util.TimeZone; @@ -59,10 +58,7 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; import freemarker.cache.ClassTemplateLoader; import freemarker.cache.FileTemplateLoader; @@ -158,6 +154,19 @@ public final class Transform { static final String SETTING_NUMBERED_SECTIONS = "numberedSections"; static final String SETTING_CUSTOM_VARIABLES = "customVariables"; static final String SETTING_INSERTABLE_FILES = "insertableFiles"; + static final String SETTING_INSERTABLE_OUTPUT_COMMANDS = "insertableOutputCommands"; + static final String SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY = "class"; + static final String SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY = "prependedArguments"; + static final String SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY = "workDirectory"; + static final Set<String> SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS; + static final Set<String> SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS; + static { + SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS = new LinkedHashSet<>(); + SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS.add(SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY); + SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS = new LinkedHashSet<>(); + SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS.add(SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY); + SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS.add(SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY); + } static final String SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE = "programlistingsRequireRole"; @@ -428,6 +437,8 @@ public final class Transform { private final Map<String, String> insertableFilesFromSettingsFile = new HashMap<>(); private final Map<String, String> insertableFilesOverrides = new HashMap<>(); + private final Map<String, InsertableOutputCommandProperties> insertableOutputCommands = new HashMap<>(); + private final LinkedHashMap<String, String> tabs = new LinkedHashMap<>(); private final Map<String, Map<String, String>> secondaryTabs = new LinkedHashMap<>(); @@ -537,10 +548,9 @@ public final class Transform { if (cfgFile.exists()) { Map<String, Object> cfg; try { - cfg = CJSONInterpreter.evalAsMap(cfgFile); + cfg = CJSONInterpreter.evalAsMap(cfgFile, new DocgenCJSONEvaluationEnvironment(), false); } catch (CJSONInterpreter.EvaluationException e) { - throw new DocgenException(e.getMessage(), - e.getCause()); + throw new DocgenException(e.getMessage(), e.getCause()); } for (Entry<String, Object> cfgEnt : cfg.entrySet()) { @@ -607,6 +617,36 @@ public final class Transform { insertableFilesFromSettingsFile.putAll( // Allow null values in the Map, as the caller can override them. castSettingToMap(settingName, settingValue, String.class, String.class, true)); + } else if (topSettingName.equals(SETTING_INSERTABLE_OUTPUT_COMMANDS)) { + Map<String, Map<String, Object>> m = castSetting( + settingName, settingValue, + Map.class, + new MapEntryType(String.class, Map.class), + new MapEntryType( + String.class, SETTING_INSERTABLE_OUTPUT_COMMADS_REQUIRED_KEYS, SETTING_INSERTABLE_OUTPUT_COMMADS_OPTIONAL_KEYS, + Object.class, false)); + for (Entry<String, Map<String, Object>> ent : m.entrySet()) { + String commandKey = ent.getKey(); + Map<String, Object> outputCmdProps = ent.getValue(); + InsertableOutputCommandProperties commandProps = new InsertableOutputCommandProperties( + castSetting( + settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY), + outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMADS_CLASS_KEY), + String.class + ), + castSetting( + settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY), + outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMADS_PREPENDED_ARGUMENTS_KEY), + List.class + ), + Paths.get(castSetting( + settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY), + outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMADS_WORK_DIRECTORY_KEY), + String.class + )) + ); + insertableOutputCommands.put(commandKey, commandProps); + } } else if (topSettingName.equals(SETTING_TABS)) { tabs.putAll( castSettingToMap(settingName, settingValue, String.class, String.class)); @@ -1151,8 +1191,8 @@ public final class Transform { private Map<String, Object> computeCustomVariables() throws DocgenException { for (String varName : customVariableOverrides.keySet()) { if (!customVariablesFromSettingsFile.containsKey(varName)) { - throw new DocgenException("Attempt to set custom variable " + StringUtil.jQuote(varName) - + ", when it was not set in the settings file (" + FILE_SETTINGS + ")."); + throw new DocgenException("Attempt to override custom variable " + StringUtil.jQuote(varName) + + ", when it was not set in the settings file (" + cfgFile + ")."); } } @@ -2500,7 +2540,11 @@ public final class Transform { return insertableFiles; } - // ------------------------------------------------------------------------- + public Map<String, InsertableOutputCommandProperties> getInsertableOutputCommands() { + return insertableOutputCommands; + } + +// ------------------------------------------------------------------------- public File getDestinationDirectory() { return destDir; @@ -2734,4 +2778,85 @@ public final class Transform { } } + static class InsertableOutputCommandProperties { + private final String mainClassName; + private final List<String> prependedArguments; + private final Path workDirectory; + + public InsertableOutputCommandProperties(String mainClassName, List<String> prependedArguments, Path workDirectory) { + this.mainClassName = mainClassName; + this.prependedArguments = prependedArguments; + this.workDirectory = workDirectory; + } + + @Override + public String toString() { + return "InsertableOutputCommandProperties{" + + "mainClassName='" + mainClassName + '\'' + + ", prependedArguments=" + prependedArguments + + ", workDirectory=" + workDirectory + '}'; + } + } + + @FunctionalInterface + interface CJSONFunction { + Object run(Transform context, CJSONInterpreter.FunctionCall fc); + } + + private static final Map<String, CJSONFunction> CJSON_FUNCTIONS = ImmutableMap.of( + "getCustomVariable", + (ctx, fc) -> { + List<Object> params = fc.getParams(); + if (params.size() != 1) { + throw new DocgenException( + "CJSON function " + fc.getName() + "(name) " + + "should have 1 arguments, but had " + params.size() + "."); + } + + Object varName = params.get(0); + if (!(varName instanceof String)) { + throw new DocgenException( + "CJSON function " + fc.getName() + "(name) " + + "argument should be a string, but was a(n) " + + CJSONInterpreter.cjsonTypeNameOfValue(varName) + "."); + } + + Object result = ctx.customVariableOverrides.get(varName); + if (result == null) { + result = ctx.customVariablesFromSettingsFile.get(varName); + } + if (result == null) { + throw new DocgenException( + "The custom variable " + StringUtil.jQuote(varName) + " is not set (or was set to null)."); + } + return result; + }, + "concat", + (ctx, fc) -> { + return fc.getParams().stream() + .filter(it -> it != null) + .map(Object::toString) + .collect(Collectors.joining()); + } + ); + + class DocgenCJSONEvaluationEnvironment implements CJSONInterpreter.EvaluationEnvironment { + @Override + public Object evalFunctionCall(CJSONInterpreter.FunctionCall fc, CJSONInterpreter ip) { + String name = fc.getName(); + CJSONFunction f = CJSON_FUNCTIONS.get(name); + if (f == null) { + throw new DocgenException("Unknown CJSON function: " + name + + "\nSupported functions are: " + String.join(", ", CJSON_FUNCTIONS.keySet())); + } + return f.run(Transform.this, fc); + } + + @Override + public Object notify(CJSONInterpreter.EvaluationEvent event, CJSONInterpreter ip, String name, Object extra) throws + Exception { + return null; + } + } + } diff --git a/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/BashCommandLineArgsParserTest.java b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/BashCommandLineArgsParserTest.java new file mode 100644 index 0000000..8a9db4a --- /dev/null +++ b/freemarker-docgen-core/src/test/java/org/freemarker/docgen/core/BashCommandLineArgsParserTest.java @@ -0,0 +1,43 @@ +/* + * 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.freemarker.docgen.core; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +class BashCommandLineArgsParserTest { + + @Test + void parse() { + assertEquals(Arrays.asList(), BashCommandLineArgsParser.parse("")); + assertEquals(Arrays.asList(), BashCommandLineArgsParser.parse( " ")); + assertEquals(Arrays.asList("cmd", "1", "2", "3"), BashCommandLineArgsParser.parse("cmd 1\t2\r\n3")); + assertEquals(Arrays.asList("1 x", "2 x", "a'bcd"), BashCommandLineArgsParser.parse("'1 x' \"2 x\" a\"'\"b'c'd")); + assertEquals(Arrays.asList("abc"), BashCommandLineArgsParser.parse("a\\bc")); + assertEquals(Arrays.asList("a\\bc"), BashCommandLineArgsParser.parse("a\\\\bc")); + assertEquals(Arrays.asList("a'bc", "d e"), BashCommandLineArgsParser.parse("a\\'bc d\\ \\ e")); + assertEquals(Arrays.asList("a\\b\\c\"$"), BashCommandLineArgsParser.parse("\"a\\b\\\\c\\\"\\$\"")); + assertEquals(Arrays.asList("a\\b\\\\c"), BashCommandLineArgsParser.parse("'a\\b\\\\c'")); + } + +} \ No newline at end of file
