/* * $Id$ * * SARL is an general-purpose agent programming language. * More details on http://www.sarl.io * * Copyright (C) 2014-2017 the original authors 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 io.sarl.maven.docs.parser; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.lang.ref.WeakReference; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Named; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.inject.Inject; import com.google.inject.Injector; import org.apache.commons.lang3.tuple.MutableTriple; import org.arakhne.afc.vmutil.FileSystem; import org.arakhne.afc.vmutil.ReflectionUtil; import org.eclipse.jdt.core.Flags; import org.eclipse.xtext.Constants; import org.eclipse.xtext.xbase.lib.Functions; import org.eclipse.xtext.xbase.lib.Functions.Function2; import org.eclipse.xtext.xbase.lib.Procedures; import org.eclipse.xtext.xbase.lib.Procedures.Procedure1; import io.sarl.lang.actionprototype.IActionPrototypeProvider; import io.sarl.lang.annotation.DefaultValue; import io.sarl.lang.annotation.SyntheticMember; import io.sarl.lang.util.OutParameter; import io.sarl.lang.util.Utils; import io.sarl.maven.docs.testing.ScriptExecutor; /** Generator of the marker language files for the modified marker language for SARL. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ public class SarlDocumentationParser { /** Default pattern for formatting inline code. */ public static final String DEFAULT_INLINE_FORMAT = "`{0}`"; //$NON-NLS-1$ /** Default string to put for the outline location. */ public static final String DEFAULT_OUTLINE_OUTPUT_TAG = "[::Outline::]"; //$NON-NLS-1$ /** Default text for line continuation. */ public static final String DEFAULT_LINE_CONTINUATION = " "; //$NON-NLS-1$ private static final String DEFAULT_TAG_NAME_PATTERN = "\\[:(.*?)[:!]?\\]"; //$NON-NLS-1$ private static final int PATTERN_COMPILE_OPTIONS = Pattern.DOTALL | Pattern.MULTILINE; private Injector injector; private IActionPrototypeProvider actionPrototypeProvider; private Map<Tag, String> rawPatterns = new HashMap<>(); private Map<Tag, Pattern> compiledPatterns = new HashMap<>(); private String inlineFormat; private Function2<String, String, String> blockFormat; private String outlineOutputTag; private String dynamicNameExtractionPattern; private Collection<Properties> additionalPropertyProviders = new ArrayList<>(); private String lineSeparator; private String languageName; private ScriptExecutor scriptExecutor; private String lineContinuation; /** Constructor. */ public SarlDocumentationParser() { reset(); } /** Change the injector. * * @param injector the injector. */ @Inject public void setInjector(Injector injector) { assert injector != null; this.injector = injector; } /** Change the name of the language. * * @param outputLanguage the language name, or {@code null} for ignoring the language name. */ @Inject public void setOutputLanguage(@Named(Constants.LANGUAGE_NAME) String outputLanguage) { if (!Strings.isNullOrEmpty(outputLanguage)) { final String[] parts = outputLanguage.split("\\.+"); //$NON-NLS-1$ if (parts.length > 0) { final String simpleName = parts[parts.length - 1]; if (!Strings.isNullOrEmpty(simpleName)) { this.languageName = simpleName; return; } } } this.languageName = null; } /** Change the script executor. * * @param executor the script executor. */ @Inject public void setScriptExecutor(ScriptExecutor executor) { this.scriptExecutor = executor; } /** Replies the script executor. * * @return the script executor. */ public ScriptExecutor getScriptExecutor() { return this.scriptExecutor; } /** Replies the string of character to put in the text when line continuation is detected. * * @return the line continuation string of characters, or {@code null} to ignore line continuations. */ public String getLineContinuation() { return this.lineContinuation; } /** Change the string of character to put in the text when line continuation is detected. * * @param lineContinuationText the line continuation string of characters, or {@code null} to ignore line continuations. */ public void setLineContinuation(String lineContinuationText) { this.lineContinuation = lineContinuationText; } /** Replies the fenced code block formatter. * * <p>This code block formatter is usually used by Github. * * @return the formatter. */ public static Function2<String, String, String> getFencedCodeBlockFormatter() { return (languageName, content) -> { return "```" + Strings.nullToEmpty(languageName).toLowerCase() + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + content + "```\n"; //$NON-NLS-1$ }; } /** Replies the basic code block formatter. * * @return the formatter. */ public static Function2<String, String, String> getBasicCodeBlockFormatter() { return (languageName, content) -> { return Pattern.compile("^", Pattern.MULTILINE).matcher(content).replaceAll("\t"); //$NON-NLS-1$ //$NON-NLS-2$ }; } /** Replies the name of the language. * * @return the language name, or {@code null} for ignoring the language name. */ public String getOutputLanguage() { return this.languageName; } /** Change the provider of action prototypes. * * @param provider the provider. */ @Inject public void setActionPrototypeProvider(IActionPrototypeProvider provider) { assert provider != null; this.actionPrototypeProvider = provider; } /** Replies the provider of action prototypes. * * @return the provider. */ @Inject public IActionPrototypeProvider getActionPrototypeProvider() { return this.actionPrototypeProvider; } /** Reset to the default settings. */ public void reset() { this.rawPatterns.clear(); this.compiledPatterns.clear(); this.inlineFormat = DEFAULT_INLINE_FORMAT; this.blockFormat = null; this.outlineOutputTag = DEFAULT_OUTLINE_OUTPUT_TAG; this.dynamicNameExtractionPattern = DEFAULT_TAG_NAME_PATTERN; this.lineContinuation = DEFAULT_LINE_CONTINUATION; } /** Add a provider of properties that could be used for finding replacement values. * * @param properties the property provider. */ public void addPropertyProvider(Properties properties) { this.additionalPropertyProviders.add(properties); } /** Replies additional providers of properties that could be used for finding replacement values. * * @return the property providers. */ public Iterable<Properties> getAdditionalPropertyProviders() { return Collections.unmodifiableCollection(this.additionalPropertyProviders); } /** Change the pattern of the tag. * * @param tag the tag. * @param regex the regular expression. */ public void setPattern(Tag tag, String regex) { if (Strings.isNullOrEmpty(regex)) { this.rawPatterns.remove(tag); this.compiledPatterns.remove(tag); } else { this.rawPatterns.put(tag, regex); this.compiledPatterns.put(tag, Pattern.compile("^\\s*" + regex, PATTERN_COMPILE_OPTIONS)); //$NON-NLS-1$ } } /** Replies the pattern of the tag. * * @param tag the tag. * @return the regular expression pattern. */ public String getPattern(Tag tag) { final String pattern = this.rawPatterns.get(tag); if (pattern == null) { return tag.getDefaultPattern(); } return pattern; } /** Replies the tag that is matching the given text. * * @param text the text to match. * @return the tag or {@code null}. */ public Tag getTagForPattern(CharSequence text) { for (final Tag tag : Tag.values()) { Pattern pattern = this.compiledPatterns.get(tag); if (pattern == null) { pattern = Pattern.compile("^\\s*" + getPattern(tag), Pattern.DOTALL); //$NON-NLS-1$ this.compiledPatterns.put(tag, pattern); } final Matcher matcher = pattern.matcher(text); if (matcher.find()) { return tag; } } return null; } /** Change the pattern for the failure tag. * * @param regex the regular expression. */ public final void setFailurePattern(String regex) { setPattern(Tag.FAILURE, regex); } /** Replies the pattern for the failure tag. * * @return the regular expression. */ public final String getFailurePattern() { return getPattern(Tag.FAILURE); } /** Change the pattern for the success tag. * * @param regex the regular expression. */ public final void setSuccessPattern(String regex) { setPattern(Tag.SUCCESS, regex); } /** Replies the pattern for the success tag. * * @return the regular expression. */ public final String getSuccessPattern() { return getPattern(Tag.SUCCESS); } /** Change the pattern for the definition tag. * * @param regex the regular expression. */ public final void setDefinitionPattern(String regex) { setPattern(Tag.DEFINITION, regex); } /** Replies the pattern for the definition tag. * * @return the regular expression. */ public final String getDefinitionPattern() { return getPattern(Tag.DEFINITION); } /** Change the pattern for the reference tag. * * @param regex the regular expression. */ public final void setReferencePattern(String regex) { setPattern(Tag.REFERENCE, regex); } /** Replies the pattern for the reference tag. * * @return the regular expression. */ public final String getReferencePattern() { return getPattern(Tag.REFERENCE); } /** Change the pattern for the "off" tag. * * @param regex the regular expression. */ public final void setOffPattern(String regex) { setPattern(Tag.OFF, regex); } /** Replies the pattern for the "off" tag. * * @return the regular expression. */ public final String getOffPattern() { return getPattern(Tag.OFF); } /** Change the pattern for the "on" tag. * * @param regex the regular expression. */ public final void setOnPattern(String regex) { setPattern(Tag.ON, regex); } /** Replies the pattern for the "on" tag. * * @return the regular expression. */ public final String getOnPattern() { return getPattern(Tag.ON); } /** Change the pattern for the fact tag. * * @param regex the regular expression. */ public final void setFactPattern(String regex) { setPattern(Tag.FACT, regex); } /** Replies the pattern for the fact tag. * * @return the regular expression. */ public final String getFactPattern() { return getPattern(Tag.FACT); } /** Change the pattern for the include tag. * * @param regex the regular expression. */ public final void setIncludePattern(String regex) { setPattern(Tag.INCLUDE, regex); } /** Replies the pattern for the include tag. * * @return the regular expression. */ public final String getIncludePattern() { return getPattern(Tag.INCLUDE); } /** Change the pattern for the outline tag. * * @param regex the regular expression. */ public final void setOutlinePattern(String regex) { setPattern(Tag.OUTLINE, regex); } /** Replies the pattern for the outline tag. * * @return the regular expression. */ public final String getOutlinePattern() { return getPattern(Tag.OUTLINE); } /** Set the template for inline codes. * * <p>The template should follow the {@link MessageFormat} specifications. * * @param template the template. */ public void setInlineCodeTemplate(String template) { if (!Strings.isNullOrEmpty(template)) { this.inlineFormat = template; } } /** Replies the template for inline codes. * * <p>The template should follow the {@link MessageFormat} specifications. * * @return the template. */ public String getInlineCodeTemplate() { return this.inlineFormat; } /** Set the template for block codes. * * <p>The first parameter of the function is the language name. The second parameter is * the code to format. * * @param template the template. */ public void setBlockCodeTemplate(Function2<String, String, String> template) { this.blockFormat = template; } /** Replies the template for block codes. * * <p>The first parameter of the function is the language name. The second parameter is * the code to format. * * @return the template. */ public Function2<String, String, String> getBlockCodeTemplate() { return this.blockFormat; } /** Change the outline output tag that will be output when the outline * tag is found in the input content. * * @param tag the tag for outline. */ protected void setOutlineOutputTag(String tag) { this.outlineOutputTag = tag; } /** Replies the outline output tag that will be output when the outline * tag is found in the input content. * * @return the tag. */ public String getOutlineOutputTag() { return this.outlineOutputTag; } /** Set the regular expression for extracting the dynamic name of a tag string. * * @param pattern the regex. */ public void setDynamicNameExtractionPattern(String pattern) { if (!Strings.isNullOrEmpty(pattern)) { this.dynamicNameExtractionPattern = pattern; } } /** Replies the regular expression for extracting the dynamic name of a tag string. * * @return the regex. */ public String getDynamicNameExtractionPattern() { return this.dynamicNameExtractionPattern; } /** Replies the OS-dependent line separator. * * @return the line separator from the {@code "line.separator"} property, or {@code "\n"}. */ public String getLineSeparator() { if (Strings.isNullOrEmpty(this.lineSeparator)) { final String nl = System.getProperty("line.separator"); //$NON-NLS-1$ if (Strings.isNullOrEmpty(nl)) { return "\n"; //$NON-NLS-1$ } return nl; } return this.lineSeparator; } /** Change the OS-dependent line separator. * * @param lineSeparator the line separator or {@code null} to use the system configuration. */ public void setLineSeparator(String lineSeparator) { this.lineSeparator = lineSeparator; } private String buildGeneralTagPattern() { final StringBuilder pattern = new StringBuilder(); int nbTags = 0; for (final Tag tag : Tag.values()) { if (nbTags >= 1) { pattern.append("|"); //$NON-NLS-1$ } pattern.append("(?:"); //$NON-NLS-1$ if (tag.isEnclosingSpaceCouldRemovable()) { pattern.append("[ \\t]*"); //$NON-NLS-1$ } pattern.append("("); //$NON-NLS-1$ pattern.append(getPattern(tag)); pattern.append(")"); //$NON-NLS-1$ if (tag.hasParameter()) { pattern.append("(?:"); //$NON-NLS-1$ pattern.append("(?:\\(\\s*([^\\)]*?)\\s*\\))|"); //$NON-NLS-1$ pattern.append("(?:\\{\\s*([^\\}]*?)\\s*\\})|"); //$NON-NLS-1$ pattern.append("(?:\\|\\s*([^\\|]*?)\\s*\\|)|"); //$NON-NLS-1$ pattern.append("(?:\\$\\s*([^\\$]*?)\\s*\\$)"); //$NON-NLS-1$ pattern.append(")"); //$NON-NLS-1$ } if (tag.isEnclosingSpaceCouldRemovable()) { pattern.append("[ \\t]*"); //$NON-NLS-1$ } pattern.append(")"); //$NON-NLS-1$ ++nbTags; } if (nbTags > 0) { pattern.insert(0, "(" + org.eclipse.xtext.util.Strings.convertToJavaString(getLineSeparator()) //$NON-NLS-1$ + ")|"); //$NON-NLS-1$ return pattern.toString(); } return null; } /** Extract the dynamic name of that from the raw text. * * @param tag the tag to extract for. * @param name the raw text. * @param dynamicName the dynamic name. */ protected void extractDynamicName(Tag tag, CharSequence name, OutParameter<String> dynamicName) { if (tag.hasDynamicName()) { final Pattern pattern = Pattern.compile(getDynamicNameExtractionPattern()); final Matcher matcher = pattern.matcher(name); if (matcher.matches()) { dynamicName.set(Strings.nullToEmpty(matcher.group(1))); return; } } dynamicName.set(name.toString()); } private static int findFirstGroup(Matcher matcher, int startIdx) { final int len = matcher.groupCount(); for (int i = startIdx + 1; i <= len; ++i) { final String value = matcher.group(i); if (value != null) { return i; } } return 1; } /** Read the given file and transform its content in order to have a raw text. * * @param inputFile the input file. * @return the raw file context. */ public String transform(File inputFile) { final String content; try (FileReader reader = new FileReader(inputFile)) { content = read(reader); } catch (IOException exception) { reportError(Messages.SarlDocumentationParser_0, exception); return null; } return transform(content, inputFile); } /** Read the given input stream and transform its content in order to have a raw text. * * @param reader the input stream. * @param inputFile the name of the input file for locating included features and formatting error messages. * @return the raw file context. */ public String transform(Reader reader, File inputFile) { final String content; try { content = read(reader); } catch (IOException exception) { reportError(Messages.SarlDocumentationParser_0, exception); return null; } return transform(content, inputFile); } /** Read the given input content and transform it in order to have a raw text. * * @param content the content to parse. * @param inputFile the name of the input file for locating included features and formatting error messages. * @return the raw file context. */ public String transform(CharSequence content, File inputFile) { final ParsingContext rootContextForReplacements = new ParsingContext(); initializeContext(rootContextForReplacements); CharSequence rawContent = preProcessing(content); Stage stage = Stage.first(); do { final ContentParserInterceptor interceptor = new ContentParserInterceptor(); // Reset the lineno because it is not reset between the different stages. rootContextForReplacements.setLineNo(1); final boolean hasChanged = parse(rawContent, inputFile, 0, stage, rootContextForReplacements, interceptor); if (hasChanged) { rawContent = interceptor.getResult(); } stage = stage.next(); } while (stage != null); return postProcessing(rawContent); } /** Do a pre processing of the text. * * @param text the text to pre process. * @return the pre-processed text. */ @SuppressWarnings("static-method") protected CharSequence preProcessing(CharSequence text) { return text; } /** Do a post processing of the text. * * @param text the text to post process. * @return the post-processed text. */ protected String postProcessing(CharSequence text) { final String lineContinuation = getLineContinuation(); if (lineContinuation != null) { final Pattern pattern = Pattern.compile( "\\s*\\\\[\\n\\r]+\\s*", //$NON-NLS-1$ Pattern.DOTALL); final Matcher matcher = pattern.matcher(text.toString().trim()); return matcher.replaceAll(lineContinuation); } return text.toString().trim(); } /** Read the given input content and extract validation components. * * @param inputFile the input file. * @param observer the oberserver to be called with extracted information. The parameter of the lambda maps * the tags to the associated list of the extraction information. */ public void extractValidationComponents(File inputFile, Procedure1<Map<Tag, List<MutableTriple<File, Integer, String>>>> observer) { final String content; try (FileReader reader = new FileReader(inputFile)) { content = read(reader); } catch (IOException exception) { reportError(Messages.SarlDocumentationParser_0, exception); return; } extractValidationComponents(content, inputFile, observer); } /** Read the given input content and extract validation components. * * @param reader the input stream. * @param inputFile the name of the input file for locating included features and formatting error messages. * @param observer the oberserver to be called with extracted information. The parameter of the lambda maps * the tags to the associated list of the extraction information. */ public void extractValidationComponents(Reader reader, File inputFile, Procedure1<Map<Tag, List<MutableTriple<File, Integer, String>>>> observer) { final String content; try { content = read(reader); } catch (IOException exception) { reportError(Messages.SarlDocumentationParser_0, exception); return; } extractValidationComponents(content, inputFile, observer); } /** Read the given input content and extract validation components. * * @param content the content to parse. * @param inputFile the name of the input file for locating included features and formatting error messages. * @param observer the oberserver to be called with extracted information. The parameter of the lambda maps * the tags to the associated list of the extraction information. */ public void extractValidationComponents(CharSequence content, File inputFile, Procedure1<Map<Tag, List<MutableTriple<File, Integer, String>>>> observer) { // // STEP 1: Extract the raw text // final Map<Tag, List<MutableTriple<File, Integer, String>>> components = new TreeMap<>(); final ContentParserInterceptor interceptor = new ContentParserInterceptor(new ParserInterceptor() { @Override public void tag(ParsingContext context, Tag tag, String dynamicName, String parameter, String blockValue) { if (tag.isOpeningTag() || tag.hasParameter()) { List<MutableTriple<File, Integer, String>> values = components.get(tag); if (values == null) { values = new ArrayList<>(); components.put(tag, values); } if (tag.isOpeningTag()) { values.add(new MutableTriple<>(context.getCurrentFile(), context.getLineNo(), Strings.nullToEmpty(blockValue).trim())); } else { values.add(new MutableTriple<>(context.getCurrentFile(), context.getLineNo(), Strings.nullToEmpty(parameter).trim())); } } } }); final ParsingContext rootContextForReplacements = new ParsingContext(true); initializeContext(rootContextForReplacements); parse(content, inputFile, 0, Stage.FIRST, rootContextForReplacements, interceptor); // // STEP 2: Do macro replacement in the captured elements. // final Collection<List<MutableTriple<File, Integer, String>>> allTexts = new ArrayList<>(components.values()); for (final List<MutableTriple<File, Integer, String>> values : allTexts) { for (final MutableTriple<File, Integer, String> pair : values) { final ContentParserInterceptor localInterceptor = new ContentParserInterceptor(interceptor); parse(pair.getRight(), inputFile, 0, Stage.SECOND, rootContextForReplacements, localInterceptor); final String newCapturedText = localInterceptor.getResult(); pair.setRight(newCapturedText); } } observer.apply(components); } /** Initialize the given context. * * @param context the context. */ protected void initializeContext(ParsingContext context) { context.setLineNo(1); context.setScriptExecutor(getScriptExecutor()); } /** Parse the given source text. * * @param source the source text. * @param file the file to be read. * @param startIndex the start index in the file. * @param stage the number of the stage to run. * @param parentContext the parent context, or {@code null} if none. * @param interceptor interceptor of the parsed elements. * @return {@code true} if a special tag was found. */ protected boolean parse(CharSequence source, File file, int startIndex, Stage stage, ParsingContext parentContext, ParserInterceptor interceptor) { final ParsingContext context = this.injector.getInstance(ParsingContext.class); context.setParser(this); context.setText(source); context.setCurrentFile(file); context.setStartIndex(startIndex); context.setParserInterceptor(interceptor); context.setInlineCodeFormat(getInlineCodeTemplate()); context.setBlockCodeFormat(getBlockCodeTemplate()); context.setOutlineOutputTag(getOutlineOutputTag()); context.setLineSeparator(getLineSeparator()); context.setOutputLanguage(getOutputLanguage()); context.setStage(stage); final String regex = buildGeneralTagPattern(); if (!Strings.isNullOrEmpty(regex)) { final Pattern patterns = Pattern.compile(regex, PATTERN_COMPILE_OPTIONS); final Matcher matcher = patterns.matcher(source); context.setMatcher(matcher); if (parentContext != null) { context.linkTo(parentContext); } return parse(context); } return false; } /** Parse the given source text. * * @param context the parsing context. * @return {@code true} if a special tag was found. */ protected boolean parse(ParsingContext context) { try { context.getParserInterceptor().openContext(context); boolean specialTagFound = false; final String lineSeparator = getLineSeparator(); while (context.getMatcher().find()) { int groupIndex = findFirstGroup(context.getMatcher(), 0); final String tagName = context.getMatcher().group(groupIndex); if (lineSeparator.equals(tagName)) { context.incrementLineNo(); continue; } final Tag tag = getTagForPattern(tagName); if (tag != null) { if (tag.isActive(context)) { final String tagDynamicName; if (tag.hasDynamicName()) { final OutParameter<String> dname = new OutParameter<>(); extractDynamicName(tag, tagName, dname); tagDynamicName = dname.get(); } else { tagDynamicName = null; } final String parameterValue; if (tag.hasParameter()) { groupIndex = findFirstGroup(context.getMatcher(), groupIndex); final String parameter = Strings.nullToEmpty(context.getMatcher().group(groupIndex)); final ContentParserInterceptor subInterceptor = new ContentParserInterceptor(context.getParserInterceptor()); final boolean inBlock = context.setInBlock(false); final boolean inParam = context.setInParameter(true); parse(parameter, context.getCurrentFile(), context.getMatcher().start(groupIndex) + context.getStartIndex(), context.getStage(), context, subInterceptor); context.setInParameter(inParam); context.setInBlock(inBlock); parameterValue = Strings.emptyToNull(subInterceptor.getResult()); } else { parameterValue = null; } final String blockContent; if (tag.isOpeningTag()) { groupIndex = findFirstGroup(context.getMatcher(), groupIndex); final String tagContent = context.getMatcher().group(groupIndex); final ContentParserInterceptor subInterceptor = new ContentParserInterceptor(context.getParserInterceptor()); final boolean inBlock = context.setInBlock(true); final boolean inParam = context.setInParameter(false); context.setVisibleInBlock(false); parse(Strings.nullToEmpty(tagContent), context.getCurrentFile(), context.getMatcher().start(groupIndex) + context.getStartIndex(), context.getStage(), context, subInterceptor); context.setInParameter(inParam); context.setInBlock(inBlock); blockContent = Strings.nullToEmpty(subInterceptor.getResult()); } else { blockContent = null; } specialTagFound = true; context.getParserInterceptor().tag(context, tag, tagDynamicName, parameterValue, blockContent); } else { // Ignore the tag for this stage. } } else { reportError(context, Messages.SarlDocumentationParser_1, tagName); return false; } } context.getParserInterceptor().closeContext(context); return specialTagFound; } catch (ParsingException exception) { throw exception; } catch (Throwable exception) { final Throwable rootException = Throwables.getRootCause(exception); throw new ParsingException( rootException.getClass().getName() + " - " + rootException.getLocalizedMessage(), //$NON-NLS-1$ context.getCurrentFile(), context.getLineNo(), rootException); } } /** Report an error. * * @param context the context. * @param message the message in a format compatible with {@link MessageFormat}. The first argument is * the filename. The second argument is the line number. The third argument is the position in the file. * @param parameter additional parameter, starting at {4}. */ protected static void reportError(ParsingContext context, String message, Object parameter) { final File file = context.getCurrentFile(); final int offset = context.getMatcher().start() + context.getStartIndex(); final int lineno = context.getLineNo(); Throwable cause = null; if (parameter instanceof Throwable) { cause = (Throwable) parameter; } final String msg = MessageFormat.format(message, file, lineno, offset, parameter); if (cause != null) { throw new ParsingException(msg, file, lineno, cause); } throw new ParsingException(msg, file, lineno); } /** Report an error. * * @param message the message in a format compatible with {@link MessageFormat}. * @param parameters the parameters, starting at {1}. */ protected static void reportError(String message, Object... parameters) { Throwable cause = null; for (int i = 0; cause == null && i < parameters.length; ++i) { if (parameters[i] instanceof Throwable) { cause = (Throwable) parameters[i]; } } final String msg = MessageFormat.format(message, parameters); if (cause != null) { throw new ParsingException(msg, null, 1, cause); } throw new ParsingException(msg, null, 1); } /** Read the content of a file. * * @param context the current parsing context. * @param file the file to read. * @return the content. * @throws IOException if the content cannot be read. */ protected static String read(ParsingContext context, File file) throws IOException { File filename = file; if (context != null && !filename.isAbsolute()) { filename = FileSystem.makeAbsolute(filename, context.getCurrentDirectory()); } try (FileReader reader = new FileReader(filename)) { return read(reader); } } /** Read the content of a file. * * @param file the file to read. * @return the content. * @throws IOException if the content cannot be read. */ protected static String read(Reader file) throws IOException { final StringBuilder content = new StringBuilder(); try (BufferedReader reader = new BufferedReader(file)) { String line = reader.readLine(); boolean first = true; while (line != null) { if (first) { first = false; } else { content.append("\n"); //$NON-NLS-1$ } content.append(line); line = reader.readLine(); } } return content.toString(); } /** Format the given text in order to be suitable for being output as a block. * * @param content the text. * @param languageName the name of the output language. * @param blockFormat the formatting template. * @return the formatted text. */ protected static String formatBlockText(String content, String languageName, Function2<String, String, String> blockFormat) { String replacement = Strings.nullToEmpty(content); final String[] lines = replacement.trim().split("[\n\r]+"); //$NON-NLS-1$ int minIndent = Integer.MAX_VALUE; final Pattern wpPattern = Pattern.compile("^(\\s*)[^\\s]"); //$NON-NLS-1$ for (int i = lines.length > 1 ? 1 : 0; i < lines.length; ++i) { final String line = lines[i]; final Matcher matcher = wpPattern.matcher(line); if (matcher.find()) { final int n = matcher.group(1).length(); if (n < minIndent) { minIndent = n; if (minIndent <= 0) { break; } } } } final StringBuilder buffer = new StringBuilder(); buffer.append(lines[0]); buffer.append("\n"); //$NON-NLS-1$ for (int i = 1; i < lines.length; ++i) { final String line = lines[i]; buffer.append(line.replaceFirst("^\\s{0," + minIndent + "}", "")); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ buffer.append("\n"); //$NON-NLS-1$ } replacement = buffer.toString().replaceFirst("[\n\r]+$", "\n"); //$NON-NLS-1$ //$NON-NLS-2$ if (!Strings.isNullOrEmpty(replacement) && !"\n".equals(replacement)) { //$NON-NLS-1$ if (blockFormat != null) { return blockFormat.apply(languageName, replacement); } return replacement; } return ""; //$NON-NLS-1$ } /** Exception in parser. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ public static class ParsingException extends RuntimeException { private static final long serialVersionUID = 6654436628872799744L; private final File file; private final int lineno; /** Constructor. * * @param message the message. * @param file the file with error. * @param lineno the line number of the error. */ public ParsingException(String message, File file, int lineno) { super(message); this.file = file; this.lineno = lineno; } /** Constructor. * * @param message the message. * @param file the file with error. * @param lineno the line number of the error. * @param cause the cause of the error. */ public ParsingException(String message, File file, int lineno, Throwable cause) { super(message, cause); this.file = file; this.lineno = lineno; } /** Replies the file with error. * * @return the file. */ public File getFile() { return this.file; } /** Replies the line number of the error. * * @return the line number. */ public int getLineno() { return this.lineno; } } /** Interceptor of the parsed elements. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ public static class ParsingContext { private Map<String, String> replacements = new TreeMap<>(); private CharSequence text; private Matcher matcher; private File currentFile; private ParserInterceptor interceptor; private String inlineCodeFormat; private Function2<String, String, String> blockCodeFormat; private String outlineOutputTag; private int startIndex; private Stage stage = Stage.FIRST; private boolean inParameter; private boolean inBlock; private boolean isVisibleInblock; private boolean forceVisibility; private boolean[] isParsing = new boolean[] {true}; private WeakReference<SarlDocumentationParser> parser; private int[] lineno = new int[] {1}; private String lineSeparator; private String outputLanguage; private ScriptExecutor scriptExecutor; /** Constructor with standard visibility configuration. */ public ParsingContext() { // } /** Constructor with specific visibility configuration. * * @param forceVisibility forces all the elements to be visible. The visiblilty tags such as * {@link Tag#ON} and {@link Tag#OFF} will have no effect if the value of this argument is * {@code true}. */ public ParsingContext(boolean forceVisibility) { this.forceVisibility = forceVisibility; } /** Change the script executor. * * @param executor the script executor. */ public void setScriptExecutor(ScriptExecutor executor) { this.scriptExecutor = executor; } /** Replies the script executor. * * @return the script executor. */ public ScriptExecutor getScriptExecutor() { return this.scriptExecutor; } /** Change the name of the language. * * @param outputLanguage the language name, or {@code null} for ignoring the language name. */ public void setOutputLanguage(String outputLanguage) { this.outputLanguage = outputLanguage; } /** Replies the name of the language. * * @return the language name, or {@code null} for ignoring the language name. */ public String getOutputLanguage() { return this.outputLanguage; } /** Replies the line separator. * * @return the line separator. */ public String getLineSeparator() { return this.lineSeparator; } /** Change the line separator in the context. * * @param lineSeparator the line separator. */ public void setLineSeparator(String lineSeparator) { this.lineSeparator = lineSeparator; } /** Replies the line number. * * @return the line number. */ public int getLineNo() { return this.lineno[0]; } /** Increment the line number. */ public void incrementLineNo() { ++(this.lineno[0]); } /** Increment the line number. * * @param amount the amount. */ public void incrementLineNo(int amount) { if (amount > 0) { (this.lineno[0]) += amount; } } /** Change the line number. * * @param lineno the line number. */ public void setLineNo(int lineno) { this.lineno[0] = lineno; } @Override public String toString() { return this.interceptor.toString(); } /** Set the flag that indicates if the text is visible when inside a block. * * @param visible {@code true} if the content should be visible. */ public void setVisibleInBlock(boolean visible) { this.isVisibleInblock = visible; } /** Replies if the text is visible. * * @return {@code true} if the content is outside a block or the visibility flag is on. */ public boolean isVisible() { return !isInBlock() || this.isVisibleInblock || this.forceVisibility; } /** Set the flag that indicates if the parser is on. * * @param enable {@code true} if parser is on. */ public void setParsing(boolean enable) { this.isParsing[0] = enable; } /** Replies the flag that indicates if the parser is on. * * @return {@code true} if parser is on. */ public boolean isParsing() { return this.isParsing[0]; } /** Set if the parsing is for the content of a block tag. * * @param inblock {@code true} if the content is the content of a block. * @return the old value of the flag. */ public boolean setInBlock(boolean inblock) { final boolean old = this.inBlock; this.inBlock = inblock; return old; } /** Replies if the parsing is for the content of a block tag. * * @return {@code true} if the content is the content of a block. */ public boolean isInBlock() { return this.inBlock; } /** Set if the parsing is for the content of a tag parameter. * * @param inparam {@code true} if the content is the content of a parameter. * @return the old value of the flag. */ public boolean setInParameter(boolean inparam) { final boolean old = this.inParameter; this.inParameter = inparam; return old; } /** Replies if the parsing is for the content of a tag parameter. * * @return {@code true} if the content is the content of a parameter. */ public boolean isInParameter() { return this.inParameter; } /** Link this context to the given parent context. * * @param parentContext the parent context. */ public void linkTo(ParsingContext parentContext) { this.replacements = parentContext.replacements; this.inBlock = parentContext.inBlock; this.isParsing = parentContext.isParsing; this.isVisibleInblock = parentContext.isVisibleInblock; this.forceVisibility = parentContext.forceVisibility; this.lineno = parentContext.lineno; this.scriptExecutor = parentContext.scriptExecutor; } /** Declare a replacement. * * @param id the identifier of the replacement. * @param to the replacement value. */ public void declareReplacement(String id, String to) { this.replacements.put(id, to); } /** Get the replacement for the givene id. * * @param id the identifier of the replacement. * @return the replacement value, or {@code null} if none. */ public String getReplacement(String id) { return this.replacements.get(id); } /** Change the stage. * * @param stage the stage level. */ protected void setStage(Stage stage) { assert stage != null; this.stage = stage; } /** Replies the stage. * * @return the stage level. */ public Stage getStage() { return this.stage; } /** Change the text. * * @param text the text. */ protected void setText(CharSequence text) { this.text = text; } /** Replies the text. * * @return the text. */ public CharSequence getText() { return this.text; } /** Change the parser reference. * * @param parser the parser. */ protected void setParser(SarlDocumentationParser parser) { this.parser = new WeakReference<>(parser); } /** Replies the parser reference. * * @return the start index. */ public SarlDocumentationParser getParser() { return this.parser.get(); } /** Change the start index of the context. * * @param startIndex the start index. */ protected void setStartIndex(int startIndex) { this.startIndex = startIndex; } /** Replies the start index of the context. * * @return the start index. */ public int getStartIndex() { return this.startIndex; } /** Change the inline code format. * * @param format the inline format. */ protected void setInlineCodeFormat(String format) { this.inlineCodeFormat = format; } /** Replies the inline code format that is compatible with {@link MessageFormat}. * * <p>The first argument is the code to inline. * * @return the format. */ public String getInlineCodeFormat() { return this.inlineCodeFormat; } /** Change the block code format. * * @param format the block code format. */ protected void setBlockCodeFormat(Function2<String, String, String> format) { this.blockCodeFormat = format; } /** Replies the inline format that is compatible with {@link MessageFormat}. * * <p>The first argument is the code to put in a block. * * @return the format. */ public Function2<String, String, String> getBlockCodeFormat() { return this.blockCodeFormat; } /** Change the outline output tag that will be output when the outline * tag is found in the input content. * * @param tag the tag for outline. */ protected void setOutlineOutputTag(String tag) { this.outlineOutputTag = tag; } /** Replies the outline output tag that will be output when the outline * tag is found in the input content. * * @return the tag. */ public String getOutlineOutputTag() { return this.outlineOutputTag; } /** Change the matcher. * * @param matcher the matcher. */ protected void setMatcher(Matcher matcher) { this.matcher = matcher; } /** Replies the matcher. * * @return the matcher. */ public Matcher getMatcher() { return this.matcher; } /** Change the current file. * * @param file the current file. */ protected void setCurrentFile(File file) { this.currentFile = file; } /** Replies the current directory. * * @return the current directory. */ public File getCurrentDirectory() { return this.currentFile.getParentFile(); } /** Replies the current file. * * @return the current file. */ public File getCurrentFile() { return this.currentFile; } /** Change the parser interceptor. * * @param interceptor the interceptor. */ protected void setParserInterceptor(ParserInterceptor interceptor) { this.interceptor = interceptor; } /** Replies the parser interceptor. * * @return the parser interceptor. */ public ParserInterceptor getParserInterceptor() { return this.interceptor; } } /** Interceptor of the parsed elements. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ public interface ParserInterceptor { /** A context block has started. * * @param context the parsing context. */ default void openContext(ParsingContext context) { // } /** A context block has finished. * * @param context the parsing context. */ default void closeContext(ParsingContext context) { // } /** A simple tag was found. * * @param context the parsing context. * @param tag the found tag. * @param dynamicName the name of the tag if it could be dynamically defined, or {@code null} if not. * @param parameter the parameter value or {@code null} if no parameter was given * or the parameter value is empty. * @param blockValue the value inside the block. It is {@code null} if the tag is not a block tag. */ default void tag(ParsingContext context, Tag tag, String dynamicName, String parameter, String blockValue) { // } /** A outline tag is detected. * * @param context the parsing context. */ default void outline(ParsingContext context) { // } } /** Interceptor of the parsed elements for the parameters. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ public static class DelegateParserInterceptor implements ParserInterceptor { private ParserInterceptor delegate; /** Constructor with no delegate. */ public DelegateParserInterceptor() { // } /** Constructor delegate. * * @param delegate the delegate. */ public DelegateParserInterceptor(ParserInterceptor delegate) { this.delegate = delegate; } /** Change the delegate. * * @param delegate the delegate. */ public void setDelegate(ParserInterceptor delegate) { this.delegate = delegate; } /** Replies the delegate. * * @return the delegate. */ public ParserInterceptor getDelegate() { return this.delegate; } @Override public void openContext(ParsingContext context) { final ParserInterceptor delegate = getDelegate(); if (delegate != null) { delegate.openContext(context); } } @Override public void closeContext(ParsingContext context) { final ParserInterceptor delegate = getDelegate(); if (delegate != null) { delegate.closeContext(context); } } @Override public void tag(ParsingContext context, Tag tag, String dynamicName, String parameter, String blockValue) { final ParserInterceptor delegate = getDelegate(); if (delegate != null) { delegate.tag(context, tag, dynamicName, parameter, blockValue); } } @Override public void outline(ParsingContext context) { final ParserInterceptor delegate = getDelegate(); if (delegate != null) { delegate.outline(context); } } } /** Interceptor of the parsed elements for the parameters. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ private static class ContentParserInterceptor extends DelegateParserInterceptor { final StringBuffer buffer = new StringBuffer(); /** Constructor. * * @param delegate the delegate. */ ContentParserInterceptor(ParserInterceptor delegate) { ParserInterceptor del = delegate; while (del instanceof ContentParserInterceptor) { del = ((ContentParserInterceptor) del).getDelegate(); } setDelegate(del); } /** Constructor. */ ContentParserInterceptor() { super(); } @Override public String toString() { return getResult(); } /** Replies the value of the buffer related to the transformation process. * * <p>The result should be the one that is expected as the generated file by this parser. * * @return the value. */ public String getResult() { return this.buffer.toString(); } private StringBuffer getVisibleBuffer(boolean isVisible) { return isVisible ? this.buffer : new StringBuffer(); } @Override public void closeContext(ParsingContext context) { context.getMatcher().appendTail(getVisibleBuffer(context.isVisible())); } @Override public void tag(ParsingContext context, Tag tag, String dynamicName, String parameter, String blockValue) { final boolean isVisible = context.isVisible(); final String replacement = tag.passThrough(context, dynamicName, parameter, blockValue); context.getMatcher().appendReplacement(getVisibleBuffer(isVisible), replacement); super.tag(context, tag, dynamicName, parameter, blockValue); } } /** Definition of the special tags. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ public enum Stage { /** Stage 1: general parsing. */ FIRST { @Override public Stage next() { return SECOND; } }, /** Stage 2: Replacements with captured texts. */ SECOND { @Override public Stage next() { return null; } }; /** Replies the first stage. * * @return the first stage. */ public static Stage first() { return FIRST; } /** Replies the next stage. * * @return the next stage. */ public abstract Stage next(); } /** Definition of the special tags. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ * @since 0.6 */ public enum Tag { /** {@code [:ParserOn]} switches on the parser. */ PARSER_ON { @Override public String getDefaultPattern() { return DEFAULT_PARSERON_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { context.setParsing(true); return context.getStage() == Stage.SECOND ? "" : "[:ParserOn]"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public boolean isActive(ParsingContext context) { return true; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:ParserOff]} switches off the parser. */ PARSER_OFF { @Override public String getDefaultPattern() { return DEFAULT_PARSEROFF_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { context.setParsing(false); return context.getStage() == Stage.SECOND ? "" : "[:ParserOff]"; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public boolean isActive(ParsingContext context) { return true; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:Outline:]} is replaced by the page's outline. */ OUTLINE { @Override public String getDefaultPattern() { return DEFAULT_OUTLINE_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { context.getParserInterceptor().outline(context); final String tag = context.getOutlineOutputTag(); if (!Strings.isNullOrEmpty(tag)) { return Strings.nullToEmpty(context.getOutlineOutputTag()); } return ""; //$NON-NLS-1$ } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return true; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:Include:](path)} enables to include of files. */ INCLUDE { @Override public String getDefaultPattern() { return DEFAULT_INCLUDE_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return true; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { if (parameter == null) { reportError(context, Messages.SarlDocumentationParser_2, name()); return null; } final File filename = FileSystem.convertStringToFile(parameter); try { final String fileContent = read(context, filename); final int oldLine = context.getLineNo(); final File oldFile = context.getCurrentFile(); context.setLineNo(1); context.setCurrentFile(filename); final ContentParserInterceptor subInterceptor = new ContentParserInterceptor(context.getParserInterceptor()); context.getParser().parse( fileContent, filename, 0, context.getStage(), context, subInterceptor); context.setLineNo(oldLine); context.setCurrentFile(oldFile); return subInterceptor.getResult(); } catch (IOException exception) { reportError(context, Messages.SarlDocumentationParser_3, exception); return null; } } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:Fact:](expression)} tests the given expression to be true or not {@code null}. */ FACT { @Override public String getDefaultPattern() { return DEFAULT_FACT_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return true; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { if (context.isInBlock()) { reportError(context, Messages.SarlDocumentationParser_5, name()); return null; } return ""; //$NON-NLS-1$ } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return true; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:Dynamic:](code)} returns the text to put in the documentation text. */ DYNAMIC { @Override public String getDefaultPattern() { return DEFAULT_DYNAMIC_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return true; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { if (context.isInBlock()) { reportError(context, Messages.SarlDocumentationParser_5, name()); return null; } String code = parameter; if (Strings.isNullOrEmpty(code)) { code = blockValue; } if (!Strings.isNullOrEmpty(code)) { final ScriptExecutor executor = context.getScriptExecutor(); if (executor != null) { try { final Object result = executor.execute(context.getLineNo(), code); if (result != null) { final String stringResult = Strings.nullToEmpty(Objects.toString(result)); return stringResult; } } catch (Exception exception) { Throwables.propagate(exception); } } } return ""; //$NON-NLS-1$ } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:On]} switches on the output of the code. */ ON { @Override public String getDefaultPattern() { return DEFAULT_ON_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { context.setVisibleInBlock(true); return ""; //$NON-NLS-1$ } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:Off]} switches off the output of the code. */ OFF { @Override public String getDefaultPattern() { return DEFAULT_OFF_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { context.setVisibleInBlock(false); return ""; //$NON-NLS-1$ } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:Success:]} starts a block of code should be successfull when compiled. */ SUCCESS { @Override public String getDefaultPattern() { return DEFAULT_SUCCESS_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return true; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { if (context.isInBlock()) { reportError(context, Messages.SarlDocumentationParser_5, name()); return null; } return formatBlockText(blockValue, context.getOutputLanguage(), context.getBlockCodeFormat()); } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return true; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:Failure:]} starts a block of code should not be successfull when compiled. */ FAILURE { @Override public String getDefaultPattern() { return DEFAULT_FAILURE_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return true; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { if (context.isInBlock()) { reportError(context, Messages.SarlDocumentationParser_5, name()); return null; } return formatBlockText(blockValue, context.getOutputLanguage(), context.getBlockCodeFormat()); } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return true; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code <--- comment -->}. */ COMMENT { @Override public String getDefaultPattern() { return DEFAULT_COMMENT_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { final String text = Strings.nullToEmpty(blockValue); final String[] lines = text.split(Pattern.quote(context.getLineSeparator())); context.incrementLineNo(lines.length); return new String(); } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return true; } @Override public boolean isInternalTextAsBlockContent() { return true; } }, /** {@code [:ShowType:]} outputs the Java type with a SARL syntax. */ SHOW_TYPE { @Override public String getDefaultPattern() { return DEFAULT_SHOWTYPE_PATTERN; } @Override public boolean hasDynamicName() { return false; } @Override public boolean hasParameter() { return true; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { if (context.isInBlock() || context.isInParameter()) { reportError(context, Messages.SarlDocumentationParser_5, name()); return null; } final Class<?> javaType; try { javaType = ReflectionUtil.forName(parameter); } catch (ClassNotFoundException exception) { reportError(context, Messages.SarlDocumentationParser_0, exception); return null; } final String block; if (javaType.isInterface()) { block = extractInterface(context, javaType); } else if (javaType.isEnum()) { block = extractEnumeration(javaType); } else if (javaType.isAnnotation()) { block = extractAnnotation(context, javaType); } else { block = extractClass(context, javaType); } return formatBlockText(block, context.getOutputLanguage(), context.getBlockCodeFormat()); } private String extractInterface(ParsingContext context, Class<?> type) { final StringBuilder it = new StringBuilder(); it.append("interface ").append(type.getSimpleName()); //$NON-NLS-1$ if (type.getSuperclass() != null && !Object.class.equals(type.getSuperclass())) { it.append(" extends ").append(type.getSuperclass().getSimpleName()); //$NON-NLS-1$ } it.append(" {\n"); //$NON-NLS-1$ extractPublicMethods(context, it, type); it.append("}"); //$NON-NLS-1$ return it.toString(); } private String extractEnumeration(Class<?> type) { final StringBuilder it = new StringBuilder(); it.append("enum ").append(type.getSimpleName()); //$NON-NLS-1$ it.append(" {\n"); //$NON-NLS-1$ for (final Object cst : type.getEnumConstants()) { it.append("\t").append(((Enum<?>) cst).name()).append(",\n"); //$NON-NLS-1$ //$NON-NLS-2$ } it.append("}"); //$NON-NLS-1$ return it.toString(); } private String extractAnnotation(ParsingContext context, Class<?> type) { final StringBuilder it = new StringBuilder(); it.append("interface ").append(type.getSimpleName()); //$NON-NLS-1$ it.append(" {\n"); //$NON-NLS-1$ extractPublicMethods(context, it, type); it.append("}"); //$NON-NLS-1$ return it.toString(); } private String extractClass(ParsingContext context, Class<?> type) { final StringBuilder it = new StringBuilder(); it.append("interface ").append(type.getSimpleName()); //$NON-NLS-1$ if (type.getSuperclass() != null && !Object.class.equals(type.getSuperclass())) { it.append(" extends ").append(type.getSuperclass().getSimpleName()); //$NON-NLS-1$ } if (type.getInterfaces().length > 0) { if (type.getSuperclass() != null && !Object.class.equals(type.getSuperclass())) { it.append("\n\t\t"); //$NON-NLS-1$ } else { it.append(" "); //$NON-NLS-1$ } it.append("implements "); //$NON-NLS-1$ boolean first = true; for (final Class<?> interfaceType : type.getInterfaces()) { if (first) { first = false; } else { it.append(", "); //$NON-NLS-1$ } it.append(interfaceType.getSimpleName()); } } it.append(" {\n"); //$NON-NLS-1$ extractPublicMethods(context, it, type); it.append("}"); //$NON-NLS-1$ return it.toString(); } private boolean isDeprecated(Method method) { return Flags.isDeprecated(method.getModifiers()) || method.getAnnotation(Deprecated.class) != null; } private void extractPublicMethods(ParsingContext context, StringBuilder it, Class<?> type) { for (final Method method : type.getDeclaredMethods()) { if (Flags.isPublic(method.getModifiers()) && !Utils.isHiddenMember(method.getName()) && !isDeprecated(method) && !method.isSynthetic() && method.getAnnotation(SyntheticMember.class) == null) { it.append("\tdef ").append(method.getName()); //$NON-NLS-1$ if (method.getParameterCount() > 0) { it.append("("); //$NON-NLS-1$ boolean first = true; int i = 1; for (final Parameter param : method.getParameters()) { if (first) { first = false; } else { it.append(", "); //$NON-NLS-1$ } //it.append(param.getName()); //it.append(" : "); //$NON-NLS-1$ toType(it, param.getParameterizedType(), method.isVarArgs() && i == method.getParameterCount()); final DefaultValue defaultValue = param.getAnnotation(DefaultValue.class); if (defaultValue != null) { final String fieldName = context.getParser().getActionPrototypeProvider().toJavaArgument( "", defaultValue.value()); //$NON-NLS-1$ it.append(" = "); //$NON-NLS-1$ it.append(fieldName); } } it.append(")"); //$NON-NLS-1$ ++i; } if (method.getGenericReturnType() != null && !Objects.equals(method.getGenericReturnType(), Void.class) && !Objects.equals(method.getGenericReturnType(), void.class)) { it.append(" : "); //$NON-NLS-1$ toType(it, method.getGenericReturnType(), false); } it.append("\n"); //$NON-NLS-1$ } } } @SuppressWarnings({"checkstyle:cyclomaticcomplexity", "checkstyle:npathcomplexity"}) private void toType(StringBuilder it, Type otype, boolean isVarArg) { final Type type; if (otype instanceof Class<?>) { type = isVarArg ? ((Class<?>) otype).getComponentType() : otype; } else { type = otype; } if (type instanceof Class<?>) { it.append(((Class<?>) type).getSimpleName()); } else if (type instanceof ParameterizedType) { final ParameterizedType paramType = (ParameterizedType) type; final Type ownerType = paramType.getOwnerType(); final boolean isForFunction = ownerType != null && Functions.class.getName().equals(ownerType.getTypeName()); final boolean isForProcedure = ownerType != null && Procedures.class.getName().equals(ownerType.getTypeName()); if (!isForFunction && !isForProcedure) { it.append(((Class<?>) paramType.getRawType()).getSimpleName()); if (paramType.getActualTypeArguments().length > 0) { it.append("<"); //$NON-NLS-1$ boolean first = true; for (final Type subtype : paramType.getActualTypeArguments()) { if (first) { first = false; } else { it.append(", "); //$NON-NLS-1$ } final StringBuilder it2 = new StringBuilder(); toType(it2, subtype, false); it.append(it2); } it.append(">"); //$NON-NLS-1$ } } else { int nb = paramType.getActualTypeArguments().length; if (isForFunction) { --nb; } it.append("("); //$NON-NLS-1$ for (int i = 0; i < nb; ++i) { final Type subtype = paramType.getActualTypeArguments()[i]; if (i > 0) { it.append(", "); //$NON-NLS-1$ } toType(it, subtype, false); } it.append(") => "); //$NON-NLS-1$ if (isForFunction) { toType(it, paramType.getActualTypeArguments()[nb], false); } else { it.append("void"); //$NON-NLS-1$ } } } else if (type instanceof WildcardType) { final Type[] types = ((WildcardType) type).getUpperBounds(); toType(it, types[0], false); } else if (type instanceof GenericArrayType) { toType(it, ((GenericArrayType) type).getGenericComponentType(), false); it.append("[]"); //$NON-NLS-1$ } else { it.append(Object.class.getSimpleName()); } if (isVarArg) { it.append("*"); //$NON-NLS-1$ } } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return true; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:id:]} is replaced by the saved text with the given id with code block. */ REFERENCE { @Override public String getDefaultPattern() { return DEFAULT_REFERENCE_PATTERN; } @Override public boolean hasDynamicName() { return true; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { final String replacement = getCapturedValue(context, dynamicTag); if (!context.isInBlock() && !context.isInParameter()) { final String format = context.getInlineCodeFormat(); if (!Strings.isNullOrEmpty(format)) { return MessageFormat.format(format, replacement); } } return replacement; } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.SECOND; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:id!]} is replaced by the saved text with the given id without code block. */ RAW_REFERENCE { @Override public String getDefaultPattern() { return DEFAULT_RAW_REFERENCE_PATTERN; } @Override public boolean hasDynamicName() { return true; } @Override public boolean hasParameter() { return false; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { return getCapturedValue(context, dynamicTag); } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.SECOND; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }, /** {@code [:id](value)} saves the given text value with the given id. */ DEFINITION { @Override public String getDefaultPattern() { return DEFAULT_DEFINITION_PATTERN; } @Override public boolean hasDynamicName() { return true; } @Override public boolean hasParameter() { return true; } @Override public boolean isOpeningTag() { return false; } @Override public String passThrough(ParsingContext context, String dynamicTag, String parameter, String blockValue) { context.declareReplacement(dynamicTag, parameter); return parameter; } @Override public boolean isActive(ParsingContext context) { return context.isParsing() && context.getStage() == Stage.FIRST; } @Override public boolean isEnclosingSpaceCouldRemovable() { return false; } @Override public boolean isInternalTextAsBlockContent() { return false; } }; /** Default pattern. */ static final String DEFAULT_OUTLINE_PATTERN = "\\[:Outline:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_INCLUDE_PATTERN = "\\[:Include:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_FACT_PATTERN = "\\[:Fact:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_DYNAMIC_PATTERN = "\\[:Dynamic:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_ON_PATTERN = "\\[:On\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_OFF_PATTERN = "\\[:Off\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_REFERENCE_PATTERN = "\\[:[a-zA-Z0-9\\._]+:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_RAW_REFERENCE_PATTERN = "\\[:[a-zA-Z0-9\\._]+\\!\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_DEFINITION_PATTERN = "\\[:[a-zA-Z0-9\\._]+\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_SUCCESS_PATTERN = "\\[:Success:\\](.*?)\\[:End:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_FAILURE_PATTERN = "\\[:Failure:\\](.*?)\\[:End:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_SHOWTYPE_PATTERN = "\\[:ShowType:\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_COMMENT_PATTERN = "\\<\\!\\-{3}(.*?)\\-{2}\\>"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_PARSERON_PATTERN = "\\[:ParserOn\\]"; //$NON-NLS-1$ /** Default pattern. */ static final String DEFAULT_PARSEROFF_PATTERN = "\\[:ParserOff\\]"; //$NON-NLS-1$ /** Replies the default pattern. * * @return the pattern. */ public abstract String getDefaultPattern(); /** Replies if the tag has a parameter. * * @return {@code true} if a parameter is needed for the tag. */ public abstract boolean hasParameter(); /** Replies if the tag has a dynamic name. * * @return {@code true} if the tag may have a dynamic name. */ public abstract boolean hasDynamicName(); /** Replies if the tag is an opening tag. * * @return {@code true} if the tag is opening a block. */ public abstract boolean isOpeningTag(); /** Replies if the tag has an internal group that should be given to the * {@link #passThrough(ParsingContext, String, String, String)} * as the block content. * * @return {@code true} if the tag contains a block of text. */ public abstract boolean isInternalTextAsBlockContent(); /** Replies the string representation when this tag is interpreted. * * <p>Usually, this function is called for obtaining the raw text, * without the tags. * * @param context the parsing context. * @param dynamicName the name of the tag if it could be dynamically defined, or {@code null}. * @param parameter the value of tag parameter, or {@code null} if no parameter value. * @param blockValue the value inside the block, or {@code null} if not a block tag. * @return the raw text for the tag. */ public abstract String passThrough(ParsingContext context, String dynamicName, String parameter, String blockValue); /** Replies if the space around the tag could be removed or not. * * @return {@code true} of enclosing spaces could be removed. */ public abstract boolean isEnclosingSpaceCouldRemovable(); /** Replies if the tag is active regarding the current context. * * @param context the context. * @return {@code true} if the tag is active. */ public abstract boolean isActive(ParsingContext context); /** Replies the captured value. * * @param context the parsing context. * @param tagName the name of the tag. * @return the captured value. */ protected static String getCapturedValue(ParsingContext context, String tagName) { String replacement = null; if (!Strings.isNullOrEmpty(tagName)) { replacement = context.getReplacement(tagName); if (replacement == null) { for (final Properties provider : context.getParser().getAdditionalPropertyProviders()) { if (provider != null) { final Object obj = provider.getOrDefault(tagName, null); if (obj != null) { replacement = obj.toString(); break; } } } } if (replacement == null) { replacement = System.getProperty(tagName); } if (replacement == null) { replacement = System.getenv(tagName); } } if (replacement == null) { reportError(context, Messages.SarlDocumentationParser_4, tagName); return null; } return replacement; } } }