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 17package com.google.doclava; 18 19import com.google.clearsilver.jsilver.data.Data; 20 21import java.io.Reader; 22import java.io.IOException; 23import java.io.FileReader; 24import java.io.LineNumberReader; 25import java.util.regex.Pattern; 26import 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 */ 46public class SampleTagInfo extends TagInfo { 47 public static final SampleTagInfo[] EMPTY_ARRAY = new SampleTagInfo[0]; 48 49 public static SampleTagInfo[] getArray(int size) { 50 return size == 0 ? EMPTY_ARRAY : new SampleTagInfo[size]; 51 } 52 53 static final int STATE_BEGIN = 0; 54 static final int STATE_MATCHING = 1; 55 56 static final Pattern TEXT = 57 Pattern.compile("[\r\n \t]*([^\r\n \t]*)[\r\n \t]*([0-9A-Za-z_]*)[\r\n \t]*", Pattern.DOTALL); 58 59 private static final String BEGIN_INCLUDE = "BEGIN_INCLUDE"; 60 private static final String END_INCLUDE = "END_INCLUDE"; 61 62 private ContainerInfo mBase; 63 private String mIncluded; 64 65 public static String escapeHtml(String str) { 66 return str.replace("&", "&").replace("<", "<").replace(">", ">"); 67 } 68 69 private static boolean isIncludeLine(String str) { 70 return str.indexOf(BEGIN_INCLUDE) >= 0 || str.indexOf(END_INCLUDE) >= 0; 71 } 72 73 SampleTagInfo(String name, String kind, String text, ContainerInfo base, 74 SourcePositionInfo position) { 75 super(name, kind, text, position); 76 mBase = base; 77 78 Matcher m = TEXT.matcher(text); 79 if (!m.matches()) { 80 Errors.error(Errors.BAD_INCLUDE_TAG, position, "Bad @include tag: " + text); 81 return; 82 } 83 String filename = m.group(1); 84 String id = m.group(2); 85 boolean trim = "@sample".equals(name); 86 87 if (id == null || "".equals(id)) { 88 mIncluded = readFile(position, filename, id, trim, true, false, false); 89 } else { 90 mIncluded = loadInclude(position, filename, id, trim); 91 } 92 93 if (mIncluded == null) { 94 Errors.error(Errors.BAD_INCLUDE_TAG, position, "include tag '" + id + "' not found in file: " 95 + filename); 96 } 97 } 98 99 static String getTrimString(String line) { 100 int i = 0; 101 int len = line.length(); 102 for (; i < len; i++) { 103 char c = line.charAt(i); 104 if (c != ' ' && c != '\t') { 105 break; 106 } 107 } 108 if (i == len) { 109 return null; 110 } else { 111 return line.substring(0, i); 112 } 113 } 114 115 static String addLineNumber(String line, String num) { 116 StringBuilder numberedLine = new StringBuilder(); 117 numberedLine.append("<a class=\"number\"" + "href=\"#l" + num + "\">" + num + "\n</a>"); 118 numberedLine.append("<span class=\"code-line\" id=\"l" + num + "\">" + line + "</span>"); 119 return numberedLine.substring(0); 120 } 121 122 static String loadInclude(SourcePositionInfo pos, String filename, String id, boolean trim) { 123 Reader input = null; 124 StringBuilder result = new StringBuilder(); 125 126 String begin = BEGIN_INCLUDE + "(" + id + ")"; 127 String end = END_INCLUDE + "(" + id + ")"; 128 129 try { 130 input = new FileReader(filename); 131 LineNumberReader lines = new LineNumberReader(input); 132 133 int state = STATE_BEGIN; 134 135 int trimLength = -1; 136 String trimString = null; 137 int trailing = 0; 138 139 while (true) { 140 String line = lines.readLine(); 141 if (line == null) { 142 return null; 143 } 144 switch (state) { 145 case STATE_BEGIN: 146 if (line.indexOf(begin) >= 0) { 147 state = STATE_MATCHING; 148 } 149 break; 150 case STATE_MATCHING: 151 if (line.indexOf(end) >= 0) { 152 return result.substring(0); 153 } else { 154 boolean empty = "".equals(line.trim()); 155 if (trim) { 156 if (isIncludeLine(line)) { 157 continue; 158 } 159 if (trimLength < 0 && !empty) { 160 trimString = getTrimString(line); 161 if (trimString != null) { 162 trimLength = trimString.length(); 163 } 164 } 165 if (trimLength >= 0 && line.length() > trimLength) { 166 boolean trimThisLine = true; 167 for (int i = 0; i < trimLength; i++) { 168 if (line.charAt(i) != trimString.charAt(i)) { 169 trimThisLine = false; 170 break; 171 } 172 } 173 if (trimThisLine) { 174 line = line.substring(trimLength); 175 } 176 } 177 if (trimLength >= 0) { 178 if (!empty) { 179 for (int i = 0; i < trailing; i++) { 180 result.append('\n'); 181 } 182 line = escapeHtml(line); 183 result.append(line); 184 trailing = 1; // add \n next time, maybe 185 } else { 186 trailing++; 187 } 188 } 189 } else { 190 result.append(line); 191 result.append('\n'); 192 } 193 } 194 break; 195 } 196 } 197 } catch (IOException e) { 198 Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Error reading file for" + " include \"" + id 199 + "\" " + filename); 200 } finally { 201 if (input != null) { 202 try { 203 input.close(); 204 } catch (IOException ex) {} 205 } 206 } 207 Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Did not find " + end + " in file " + filename); 208 return null; 209 } 210 211 static String readFile(SourcePositionInfo pos, String filename, String id, boolean trim, 212 boolean escape, boolean numberedLines, boolean errorOk) { 213 Reader input = null; 214 StringBuilder result = new StringBuilder(); 215 int trailing = 0; 216 boolean started = false; 217 218 try { 219 220 input = new FileReader(filename); 221 LineNumberReader lines = new LineNumberReader(input); 222 223 while (true) { 224 String line = lines.readLine(); 225 String lineNum = Integer.toString(lines.getLineNumber()); 226 227 if (line == null) { 228 break; 229 } 230 231 if (trim) { 232 if (isIncludeLine(line)) { 233 continue; 234 } 235 if (!"".equals(line.trim())) { 236 if (started) { 237 for (int i = 0; i < trailing; i++) { 238 result.append('\n'); 239 } 240 } 241 if (escape) { 242 line = escapeHtml(line); 243 } 244 if (numberedLines) { 245 line = addLineNumber(line, lineNum); 246 } 247 result.append(line); 248 trailing = 1; // add \n next time, maybe 249 started = true; 250 } else { 251 if (started) { 252 if (numberedLines) { 253 result.append('\n'); 254 line = line + " "; 255 line = addLineNumber(line, lineNum); 256 result.append(line); 257 } else { 258 trailing++; 259 } 260 } 261 } 262 } else { 263 if (numberedLines) { 264 line = addLineNumber(line, lineNum); 265 } 266 result.append(line); 267 result.append('\n'); 268 } 269 } 270 } catch (IOException e) { 271 if (errorOk) { 272 return null; 273 } else { 274 Errors.error(Errors.BAD_INCLUDE_TAG, pos, "Error reading file for" + " include \"" + id 275 + "\" " + filename); 276 } 277 } finally { 278 if (input != null) { 279 try { 280 input.close(); 281 } catch (IOException ex) {} 282 } 283 } 284 return result.substring(0); 285 } 286 287 @Override 288 public void makeHDF(Data data, String base) { 289 data.setValue(base + ".name", name()); 290 data.setValue(base + ".kind", kind()); 291 if (mIncluded != null) { 292 data.setValue(base + ".text", mIncluded); 293 } else { 294 data.setValue(base + ".text", "INCLUDE_ERROR"); 295 } 296 } 297} 298