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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
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