1 /* 2 * Copyright (C) 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.doclava; 18 19 import com.google.clearsilver.jsilver.data.Data; 20 21 import java.io.Reader; 22 import java.io.IOException; 23 import java.io.FileReader; 24 import java.io.LineNumberReader; 25 import java.util.regex.Pattern; 26 import java.util.regex.Matcher; 27 28 /* 29 * SampleTagInfo copies text from a given file into the javadoc comment. 30 * 31 * The @include tag copies the text verbatim from the given file. 32 * 33 * The @sample tag copies the text from the given file, stripping leading and trailing whitespace, 34 * and reducing the indent level of the text to the indent level of the first non-whitespace line. 35 * 36 * Both tags accept either a filename and an id or just a filename. If no id is provided, the entire 37 * file is copied. If an id is provided, the lines in the given file between the first two lines 38 * containing BEGIN_INCLUDE(id) and END_INCLUDE(id), for the given id, are copied. The id may be 39 * only letters, numbers and underscore (_). 40 * 41 * Four examples: {@include samples/ApiDemos/src/com/google/app/Notification1.java} {@sample 42 * samples/ApiDemos/src/com/google/app/Notification1.java} {@include 43 * samples/ApiDemos/src/com/google/app/Notification1.java Bleh} {@sample 44 * samples/ApiDemos/src/com/google/app/Notification1.java Bleh} 45 */ 46 public class SampleTagInfo extends TagInfo { 47 static final int STATE_BEGIN = 0; 48 static final int STATE_MATCHING = 1; 49 50 static final Pattern TEXT = 51 Pattern.compile("[\r\n \t]*([^\r\n \t]*)[\r\n \t]*([0-9A-Za-z_]*)[\r\n \t]*", Pattern.DOTALL); 52 53 private static final String BEGIN_INCLUDE = "BEGIN_INCLUDE"; 54 private static final String END_INCLUDE = "END_INCLUDE"; 55 56 private ContainerInfo mBase; 57 private String mIncluded; 58 59 public static String escapeHtml(String str) { 60 return str.replace("&", "&").replace("<", "<").replace(">", ">"); 61 } 62 63 private static boolean isIncludeLine(String str) { 64 return str.indexOf(BEGIN_INCLUDE) >= 0 || str.indexOf(END_INCLUDE) >= 0; 65 } 66 67 SampleTagInfo(String name, String kind, String text, ContainerInfo base, 68 SourcePositionInfo position) { 69 super(name, kind, text, position); 70 mBase = base; 71 72 Matcher m = TEXT.matcher(text); 73 if (!m.matches()) { 74 Errors.error(Errors.BAD_INCLUDE_TAG, position, "Bad @include tag: " + text); 75 return; 76 } 77 String filename = m.group(1); 78 String id = m.group(2); 79 boolean trim = "@sample".equals(name); 80 81 if (id == null || "".equals(id)) { 82 mIncluded = readFile(position, filename, id, trim, true, false, false); 83 } else { 84 mIncluded = loadInclude(position, filename, id, trim); 85 } 86 87 if (mIncluded == null) { 88 Errors.error(Errors.BAD_INCLUDE_TAG, position, "include tag '" + id + "' not found in file: " 89 + filename); 90 } 91 } 92 93 static String getTrimString(String line) { 94 int i = 0; 95 int len = line.length(); 96 for (; i < len; i++) { 97 char c = line.charAt(i); 98 if (c != ' ' && c != '\t') { 99 break; 100 } 101 } 102 if (i == len) { 103 return null; 104 } else { 105 return line.substring(0, i); 106 } 107 } 108 109 static String addLineNumber(String line, String num) { 110 StringBuilder numberedLine = new StringBuilder(); 111 numberedLine.append("<a class=\"number\"" + "href=\"#l" + num + "\">" + num + "\n</a>"); 112 numberedLine.append("<span class=\"code-line\" id=\"l" + num + "\">" + line + "</span>"); 113 return numberedLine.substring(0); 114 } 115 116 static String loadInclude(SourcePositionInfo pos, String filename, String id, boolean trim) { 117 Reader input = null; 118 StringBuilder result = new StringBuilder(); 119 120 String begin = BEGIN_INCLUDE + "(" + id + ")"; 121 String end = END_INCLUDE + "(" + id + ")"; 122 123 try { 124 input = new FileReader(filename); 125 LineNumberReader lines = new LineNumberReader(input); 126 127 int state = STATE_BEGIN; 128 129 int trimLength = -1; 130 String trimString = null; 131 int trailing = 0; 132 133 while (true) { 134 String line = lines.readLine(); 135 if (line == null) { 136 return null; 137 } 138 switch (state) { 139 case STATE_BEGIN: 140 if (line.indexOf(begin) >= 0) { 141 state = STATE_MATCHING; 142 } 143 break; 144 case STATE_MATCHING: 145 if (line.indexOf(end) >= 0) { 146 return result.substring(0); 147 } else { 148 boolean empty = "".equals(line.trim()); 149 if (trim) { 150 if (isIncludeLine(line)) { 151 continue; 152 } 153 if (trimLength < 0 && !empty) { 154 trimString = getTrimString(line); 155 if (trimString != null) { 156 trimLength = trimString.length(); 157 } 158 } 159 if (trimLength >= 0 && line.length() > trimLength) { 160 boolean trimThisLine = true; 161 for (int i = 0; i < trimLength; i++) { 162 if (line.charAt(i) != trimString.charAt(i)) { 163 trimThisLine = false; 164 break; 165 } 166 } 167 if (trimThisLine) { 168 line = line.substring(trimLength); 169 } 170 } 171 if (trimLength >= 0) { 172 if (!empty) { 173 for (int i = 0; i < trailing; i++) { 174 result.append('\n'); 175 } 176 line = escapeHtml(line); 177 result.append(line); 178 trailing = 1; // add \n next time, maybe 179 } else { 180 trailing++; 181 } 182 } 183 } else { 184 result.append(line); 185 result.append('\n'); 186 } 187 } 188 break; 189 } 190 } 191 } catch (IOException e) { 192 Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Error reading file for" + " include \"" + id 193 + "\" " + filename); 194 } finally { 195 if (input != null) { 196 try { 197 input.close(); 198 } catch (IOException ex) {} 199 } 200 } 201 Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Did not find " + end + " in file " + filename); 202 return null; 203 } 204 205 static String readFile(SourcePositionInfo pos, String filename, String id, boolean trim, 206 boolean escape, boolean numberedLines, boolean errorOk) { 207 Reader input = null; 208 StringBuilder result = new StringBuilder(); 209 int trailing = 0; 210 boolean started = false; 211 212 try { 213 214 input = new FileReader(filename); 215 LineNumberReader lines = new LineNumberReader(input); 216 217 while (true) { 218 String line = lines.readLine(); 219 String lineNum = Integer.toString(lines.getLineNumber()); 220 221 if (line == null) { 222 break; 223 } 224 225 if (trim) { 226 if (isIncludeLine(line)) { 227 continue; 228 } 229 if (!"".equals(line.trim())) { 230 if (started) { 231 for (int i = 0; i < trailing; i++) { 232 result.append('\n'); 233 } 234 } 235 if (escape) { 236 line = escapeHtml(line); 237 } 238 if (numberedLines) { 239 line = addLineNumber(line, lineNum); 240 } 241 result.append(line); 242 trailing = 1; // add \n next time, maybe 243 started = true; 244 } else { 245 if (started) { 246 if (numberedLines) { 247 result.append('\n'); 248 line = line + " "; 249 line = addLineNumber(line, lineNum); 250 result.append(line); 251 } else { 252 trailing++; 253 } 254 } 255 } 256 } else { 257 if (numberedLines) { 258 line = addLineNumber(line, lineNum); 259 } 260 result.append(line); 261 result.append('\n'); 262 } 263 } 264 } catch (IOException e) { 265 if (errorOk) { 266 return null; 267 } else { 268 Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Error reading file for" + " include \"" + id 269 + "\" " + filename); 270 } 271 } finally { 272 if (input != null) { 273 try { 274 input.close(); 275 } catch (IOException ex) {} 276 } 277 } 278 return result.substring(0); 279 } 280 281 @Override 282 public void makeHDF(Data data, String base) { 283 data.setValue(base + ".name", name()); 284 data.setValue(base + ".kind", kind()); 285 if (mIncluded != null) { 286 data.setValue(base + ".text", mIncluded); 287 } else { 288 data.setValue(base + ".text", "INCLUDE_ERROR"); 289 } 290 } 291 } 292