// Copyright 2014 Google Inc. All rights reserved. // // 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 com.google.devtools.build.docgen; import com.google.common.collect.ImmutableSet; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; /** * A helper class to read and process documentations for rule classes and attributes * from exactly one java source file. */ public class SourceFileReader { private Collection ruleDocEntries; private ListMultimap attributeDocEntries; private final ConfiguredRuleClassProvider ruleClassProvider; private final String javaSourceFilePath; public SourceFileReader( ConfiguredRuleClassProvider ruleClassProvider, String javaSourceFilePath) { this.ruleClassProvider = ruleClassProvider; this.javaSourceFilePath = javaSourceFilePath; } /** * The handler class of the line read from the text file. */ public abstract static class ReadAction { // Text file line indexing starts from 1 private int lineCnt = 1; protected abstract void readLineImpl(String line) throws BuildEncyclopediaDocException, IOException; protected int getLineCnt() { return lineCnt; } public void readLine(String line) throws BuildEncyclopediaDocException, IOException { readLineImpl(line); lineCnt++; } } private static final String LS = DocgenConsts.LS; /** * Reads the attribute and rule documentation present in the file represented by * SourceFileReader.javaSourceFilePath. The rule doc variables are added to the rule * documentation (which therefore must be defined in the same file). The attribute docs are * stored in a different class member, so they need to be handled outside this method. */ public void readDocsFromComments() throws BuildEncyclopediaDocException, IOException { final Map docMap = new HashMap<>(); final List docVariables = new LinkedList<>(); final ListMultimap docAttributes = LinkedListMultimap.create(); readTextFile(javaSourceFilePath, new ReadAction() { private boolean inBlazeRuleDocs = false; private boolean inBlazeRuleVarDocs = false; private boolean inBlazeAttributeDocs = false; private StringBuilder sb = new StringBuilder(); private String ruleName; private String ruleType; private String ruleFamily; private String variableName; private String attributeName; private ImmutableSet flags; private int startLineCnt; @Override public void readLineImpl(String line) throws BuildEncyclopediaDocException { // TODO(bazel-team): check if copy paste code can be reduced using inner classes if (inBlazeRuleDocs) { if (DocgenConsts.BLAZE_RULE_END.matcher(line).matches()) { endBlazeRuleDoc(docMap); } else { appendLine(line); } } else if (inBlazeRuleVarDocs) { if (DocgenConsts.BLAZE_RULE_VAR_END.matcher(line).matches()) { endBlazeRuleVarDoc(docVariables); } else { appendLine(line); } } else if (inBlazeAttributeDocs) { if (DocgenConsts.BLAZE_RULE_ATTR_END.matcher(line).matches()) { endBlazeAttributeDoc(docAttributes); } else { appendLine(line); } } Matcher ruleStartMatcher = DocgenConsts.BLAZE_RULE_START.matcher(line); Matcher ruleVarStartMatcher = DocgenConsts.BLAZE_RULE_VAR_START.matcher(line); Matcher ruleAttrStartMatcher = DocgenConsts.BLAZE_RULE_ATTR_START.matcher(line); if (ruleStartMatcher.find()) { startBlazeRuleDoc(line, ruleStartMatcher); } else if (ruleVarStartMatcher.find()) { startBlazeRuleVarDoc(ruleVarStartMatcher); } else if (ruleAttrStartMatcher.find()) { startBlazeAttributeDoc(line, ruleAttrStartMatcher); } } private void appendLine(String line) { // Add another line of html code to the building rule documentation // Removing whitespace and java comment asterisk from the beginning of the line sb.append(line.replaceAll("^[\\s]*\\*", "") + LS); } private void startBlazeRuleDoc(String line, Matcher matcher) throws BuildEncyclopediaDocException { checkDocValidity(); // Start of a new rule String[] metaData = matcher.group(1).split(","); ruleName = readMetaData(metaData, DocgenConsts.META_KEY_NAME); ruleType = readMetaData(metaData, DocgenConsts.META_KEY_TYPE); ruleFamily = readMetaData(metaData, DocgenConsts.META_KEY_FAMILY); startLineCnt = getLineCnt(); addFlags(line); inBlazeRuleDocs = true; } private void endBlazeRuleDoc(final Map documentations) throws BuildEncyclopediaDocException { // End of a rule, create RuleDocumentation object documentations.put(ruleName, new RuleDocumentation(ruleName, ruleType, ruleFamily, sb.toString(), getLineCnt(), javaSourceFilePath, flags, ruleClassProvider)); sb = new StringBuilder(); inBlazeRuleDocs = false; } private void startBlazeRuleVarDoc(Matcher matcher) throws BuildEncyclopediaDocException { checkDocValidity(); // Start of a new rule variable ruleName = matcher.group(1).replaceAll("[\\s]", ""); variableName = matcher.group(2).replaceAll("[\\s]", ""); startLineCnt = getLineCnt(); inBlazeRuleVarDocs = true; } private void endBlazeRuleVarDoc(final List docVariables) { // End of a rule, create RuleDocumentationVariable object docVariables.add( new RuleDocumentationVariable(ruleName, variableName, sb.toString(), startLineCnt)); sb = new StringBuilder(); inBlazeRuleVarDocs = false; } private void startBlazeAttributeDoc(String line, Matcher matcher) throws BuildEncyclopediaDocException { checkDocValidity(); // Start of a new attribute ruleName = matcher.group(1).replaceAll("[\\s]", ""); attributeName = matcher.group(2).replaceAll("[\\s]", ""); startLineCnt = getLineCnt(); addFlags(line); inBlazeAttributeDocs = true; } private void endBlazeAttributeDoc( final ListMultimap docAttributes) { // End of a attribute, create RuleDocumentationAttribute object docAttributes.put(attributeName, RuleDocumentationAttribute.create( ruleClassProvider.getRuleClassDefinition(ruleName), attributeName, sb.toString(), startLineCnt, flags)); sb = new StringBuilder(); inBlazeAttributeDocs = false; } private void addFlags(String line) { // Add flags if there's any Matcher matcher = DocgenConsts.BLAZE_RULE_FLAGS.matcher(line); if (matcher.find()) { flags = ImmutableSet.copyOf(matcher.group(1).split(",")); } else { flags = ImmutableSet.of(); } } private void checkDocValidity() throws BuildEncyclopediaDocException { if (inBlazeRuleDocs || inBlazeRuleVarDocs || inBlazeAttributeDocs) { throw new BuildEncyclopediaDocException(javaSourceFilePath, getLineCnt(), "Malformed documentation, #BLAZE_RULE started after another #BLAZE_RULE."); } } }); // Adding rule doc variables to the corresponding rules for (RuleDocumentationVariable docVariable : docVariables) { if (docMap.containsKey(docVariable.getRuleName())) { docMap.get(docVariable.getRuleName()).addDocVariable( docVariable.getVariableName(), docVariable.getValue()); } else { throw new BuildEncyclopediaDocException(javaSourceFilePath, docVariable.getStartLineCnt(), String.format( "Malformed rule variable #BLAZE_RULE(%s).%s, " + "rule %s not found in file.", docVariable.getRuleName(), docVariable.getVariableName(), docVariable.getRuleName())); } } ruleDocEntries = docMap.values(); attributeDocEntries = docAttributes; } public Collection getRuleDocEntries() { return ruleDocEntries; } public ListMultimap getAttributeDocEntries() { return attributeDocEntries; } private String readMetaData(String[] metaData, String metaKey) { for (String metaDataItem : metaData) { String[] metaDataItemParts = metaDataItem.split("=", 2); if (metaDataItemParts.length != 2) { return null; } if (metaDataItemParts[0].trim().equals(metaKey)) { return metaDataItemParts[1].trim(); } } return null; } /** * Reads the template file without variable substitution. */ public static String readTemplateContents(String templateFilePath) throws BuildEncyclopediaDocException, IOException { return readTemplateContents(templateFilePath, null); } /** * Reads the template file and expands the variables. The variables has to have * the following format in the template file: ${VARIABLE_NAME}. In the Map * input parameter the key has to be VARIABLE_NAME. Variables can be null. */ public static String readTemplateContents( String templateFilePath, final Map variables) throws BuildEncyclopediaDocException, IOException { final StringBuilder sb = new StringBuilder(); readTextFile(templateFilePath, new ReadAction() { @Override public void readLineImpl(String line) { sb.append(expandVariables(line, variables) + LS); } }); return sb.toString(); } private static String expandVariables(String line, Map variables) { if (variables != null) { for (Entry variable : variables.entrySet()) { line = line.replace("${" + variable.getKey() + "}", variable.getValue()); } } return line; } public static void readTextFile(String filePath, ReadAction action) throws BuildEncyclopediaDocException, IOException { BufferedReader br = null; try { File file = new File(filePath); if (file.exists()) { br = new BufferedReader(new FileReader(file)); } else { InputStream is = SourceFileReader.class.getResourceAsStream(filePath); if (is != null) { br = new BufferedReader(new InputStreamReader(is)); } } if (br != null) { String line = null; while ((line = br.readLine()) != null) { action.readLine(line); } } else { System.out.println("Couldn't find file or resource: " + filePath); } } finally { if (br != null) { br.close(); } } } }