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 java.util.regex.Pattern;
20import java.util.regex.Matcher;
21import java.util.ArrayList;
22
23/**
24 * Class that represents what you see in an link or see tag. This is factored out of SeeTagInfo so
25 * it can be used elsewhere (like AttrTagInfo).
26 */
27public class LinkReference {
28
29  private static final boolean DBG = false;
30
31  /** The original text. */
32  public String text;
33
34  /** The kind of this tag, if we have a new suggestion after parsing. */
35  public String kind;
36
37  /** The user visible text. */
38  public String label;
39
40  /** The link. */
41  public String href;
42
43  /** Non-null for federated links */
44  public String federatedSite;
45
46  /** The {@link PackageInfo} if any. */
47  public PackageInfo packageInfo;
48
49  /** The {@link ClassInfo} if any. */
50  public ClassInfo classInfo;
51
52  /** The {@link MemberInfo} if any. */
53  public MemberInfo memberInfo;
54
55  /** The name of the referenced member PackageInfo} if any. */
56  public String referencedMemberName;
57
58  /** Set to true if everything is a-ok */
59  public boolean good;
60
61  /**
62   * regex pattern to use when matching explicit "<a href" reference text
63   */
64  private static final Pattern HREF_PATTERN =
65      Pattern.compile("^<a href=\"([^\"]*)\">([^<]*)</a>[ \n\r\t]*$", Pattern.CASE_INSENSITIVE);
66
67  /**
68   * regex pattern to use when matching double-quoted reference text
69   */
70  private static final Pattern QUOTE_PATTERN = Pattern.compile("^\"([^\"]*)\"[ \n\r\t]*$");
71
72  /**
73   * Parse and resolve a link string.
74   *
75   * @param text the original text
76   * @param base the class or whatever that this link is on
77   * @param pos the original position in the source document
78   * @return a new link reference. It always returns something. If there was an error, it logs it
79   *         and fills in href and label with error text.
80   */
81  public static LinkReference parse(String text, ContainerInfo base, SourcePositionInfo pos,
82      boolean printOnErrors) {
83    LinkReference result = new LinkReference();
84    result.text = text;
85
86    int index;
87    int len = text.length();
88    int pairs = 0;
89    int pound = -1;
90    // split the string
91    done: {
92      for (index = 0; index < len; index++) {
93        char c = text.charAt(index);
94        switch (c) {
95          case '(':
96            pairs++;
97            break;
98          case '[':
99            pairs++;
100            break;
101          case ')':
102            pairs--;
103            break;
104          case ']':
105            pairs--;
106            break;
107          case ' ':
108          case '\t':
109          case '\r':
110          case '\n':
111            if (pairs == 0) {
112              break done;
113            }
114            break;
115          case '#':
116            if (pound < 0) {
117              pound = index;
118            }
119            break;
120        }
121      }
122    }
123    if (index == len && pairs != 0) {
124      Errors.error(Errors.UNRESOLVED_LINK, pos, "unable to parse link/see tag: " + text.trim());
125      return result;
126    }
127
128    int linkend = index;
129
130    for (; index < len; index++) {
131      char c = text.charAt(index);
132      if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
133        break;
134      }
135    }
136
137    result.label = text.substring(index);
138
139    String ref;
140    String mem;
141    if (pound == 0) {
142      ref = null;
143      mem = text.substring(1, linkend);
144    } else if (pound > 0) {
145      ref = text.substring(0, pound);
146      mem = text.substring(pound + 1, linkend);
147    } else {
148      ref = text.substring(0, linkend);
149      mem = null;
150    }
151
152    // parse parameters, if any
153    String[] params = null;
154    String[] paramDimensions = null;
155    boolean varargs = false;
156    if (mem != null) {
157      index = mem.indexOf('(');
158      if (index > 0) {
159        ArrayList<String> paramList = new ArrayList<String>();
160        ArrayList<String> paramDimensionList = new ArrayList<String>();
161        len = mem.length();
162        int start = index + 1;
163        final int START = 0;
164        final int TYPE = 1;
165        final int NAME = 2;
166        int dimension = 0;
167        int arraypair = 0;
168        int state = START;
169        int typestart = 0;
170        int typeend = -1;
171        for (int i = start; i < len; i++) {
172          char c = mem.charAt(i);
173          switch (state) {
174            case START:
175              if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
176                state = TYPE;
177                typestart = i;
178              }
179              break;
180            case TYPE:
181              if (c == '.') {
182                if (mem.length() > i+2 && mem.charAt(i+1) == '.' && mem.charAt(i+2) == '.') {
183                  if (typeend < 0) {
184                    typeend = i;
185                  }
186                  varargs = true;
187                }
188              } else if (c == '[') {
189                if (typeend < 0) {
190                  typeend = i;
191                }
192                dimension++;
193                arraypair++;
194              } else if (c == ']') {
195                arraypair--;
196              } else if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
197                if (typeend < 0) {
198                  typeend = i;
199                }
200              } else {
201                if (typeend >= 0 || c == ')' || c == ',') {
202                  if (typeend < 0) {
203                    typeend = i;
204                  }
205                  String s = mem.substring(typestart, typeend);
206                  paramList.add(s);
207                  s = "";
208                  for (int j = 0; j < dimension; j++) {
209                    s += "[]";
210                  }
211                  paramDimensionList.add(s);
212                  state = START;
213                  typeend = -1;
214                  dimension = 0;
215                  if (c == ',' || c == ')') {
216                    state = START;
217                  } else {
218                    state = NAME;
219                  }
220                }
221              }
222              break;
223            case NAME:
224              if (c == ',' || c == ')') {
225                state = START;
226              }
227              break;
228          }
229
230        }
231        params = paramList.toArray(new String[paramList.size()]);
232        paramDimensions = paramDimensionList.toArray(new String[paramList.size()]);
233        mem = mem.substring(0, index);
234      }
235    }
236
237    ClassInfo cl = null;
238    if (base instanceof ClassInfo) {
239      cl = (ClassInfo) base;
240      if (DBG) System.out.println("-- chose base as classinfo");
241    }
242
243    if (ref == null) {
244      if (DBG) System.out.println("-- ref == null");
245      // no class or package was provided, assume it's this class
246      if (cl != null) {
247        if (DBG) System.out.println("-- assumed to be cl");
248        result.classInfo = cl;
249      }
250    } else {
251      if (DBG) System.out.println("-- they provided ref = " + ref);
252      // they provided something, maybe it's a class or a package
253      if (cl != null) {
254        if (DBG) System.out.println("-- cl non-null");
255        result.classInfo = cl.extendedFindClass(ref);
256        if (result.classInfo == null) {
257          if (DBG) System.out.println("-- cl.extendedFindClass was null");
258          result.classInfo = cl.findClass(ref);
259        }
260        if (result.classInfo == null) {
261          if (DBG) System.out.println("-- cl.findClass was null");
262          result.classInfo = cl.findInnerClass(ref);
263          if (DBG) if (result.classInfo == null) System.out.println("-- cl.findInnerClass was null");
264        }
265      }
266      if (result.classInfo == null) {
267        if (DBG) System.out.println("-- hitting up the Converter.obtainclass");
268        result.classInfo = Converter.obtainClass(ref);
269      }
270      if (result.classInfo == null) {
271        if (DBG) System.out.println("-- Converter.obtainClass was null");
272        result.packageInfo = Converter.obtainPackage(ref);
273      }
274    }
275
276    if (result.classInfo == null) {
277        if (DBG) System.out.println("-- NO CLASS INFO");
278    } else {
279        Doclava.federationTagger.tag(result.classInfo);
280        for (FederatedSite site : result.classInfo.getFederatedReferences()) {
281          if (DBG) System.out.println("-- reg link = " + result.classInfo.htmlPage());
282          if (DBG) System.out.println("-- fed link = " +
283              site.linkFor(result.classInfo.htmlPage()));
284        }
285    }
286
287    if (result.classInfo != null && mem != null) {
288      // it's either a field or a method, prefer a field
289      if (params == null) {
290        FieldInfo field = result.classInfo.findField(mem);
291        // findField looks in containing classes, so it might actually
292        // be somewhere else; link to where it really is, not what they
293        // typed.
294        if (field != null) {
295          result.classInfo = field.containingClass();
296          result.memberInfo = field;
297        }
298      }
299      if (result.memberInfo == null) {
300        MethodInfo method = result.classInfo.findMethod(mem, params, paramDimensions, varargs);
301        if (method != null) {
302          result.classInfo = method.containingClass();
303          result.memberInfo = method;
304        }
305      }
306    }
307
308    result.referencedMemberName = mem;
309    if (params != null) {
310      result.referencedMemberName = result.referencedMemberName + '(';
311      len = params.length;
312      if (len > 0) {
313        len--;
314        for (int i = 0; i < len; i++) {
315          result.referencedMemberName =
316              result.referencedMemberName + params[i] + paramDimensions[i] + ", ";
317        }
318        result.referencedMemberName =
319            result.referencedMemberName + params[len] + paramDimensions[len];
320      }
321      result.referencedMemberName = result.referencedMemberName + ")";
322    }
323
324    // debugging spew
325    if (false) {
326      result.label = result.label + "/" + ref + "/" + mem + '/';
327      if (params != null) {
328        for (int i = 0; i < params.length; i++) {
329          result.label += params[i] + "|";
330        }
331      }
332
333      FieldInfo f = (result.memberInfo instanceof FieldInfo) ? (FieldInfo) result.memberInfo : null;
334      MethodInfo m =
335          (result.memberInfo instanceof MethodInfo) ? (MethodInfo) result.memberInfo : null;
336      result.label =
337          result.label + "/package="
338              + (result.packageInfo != null ? result.packageInfo.name() : "") + "/class="
339              + (result.classInfo != null ? result.classInfo.qualifiedName() : "") + "/field="
340              + (f != null ? f.name() : "") + "/method=" + (m != null ? m.name() : "");
341
342    }
343
344    MethodInfo method = null;
345    boolean skipHref = false;
346
347    if (result.memberInfo != null && result.memberInfo.isExecutable()) {
348      method = (MethodInfo) result.memberInfo;
349    }
350
351    if (DBG) System.out.println("----- label = " + result.label + ", text = '" + text + "'");
352    if (text.startsWith("\"")) {
353      // literal quoted reference (e.g., a book title)
354      Matcher matcher = QUOTE_PATTERN.matcher(text);
355      if (!matcher.matches()) {
356        Errors.error(Errors.UNRESOLVED_LINK, pos, "unbalanced quoted link/see tag: " + text.trim());
357        result.makeError();
358        return result;
359      }
360      skipHref = true;
361      result.label = matcher.group(1);
362      result.kind = "@seeJustLabel";
363      if (DBG) System.out.println(" ---- literal quoted reference");
364    } else if (text.startsWith("<")) {
365      // explicit "<a href" form
366      Matcher matcher = HREF_PATTERN.matcher(text);
367      if (!matcher.matches()) {
368        Errors.error(Errors.UNRESOLVED_LINK, pos, "invalid <a> link/see tag: " + text.trim());
369        result.makeError();
370        return result;
371      }
372      result.href = matcher.group(1);
373      result.label = matcher.group(2);
374      result.kind = "@seeHref";
375      if (DBG) System.out.println(" ---- explicit href reference");
376    } else if (result.packageInfo != null) {
377      result.href = result.packageInfo.htmlPage();
378      if (result.label.length() == 0) {
379        result.href = result.packageInfo.htmlPage();
380        result.label = result.packageInfo.name();
381      }
382      if (DBG) System.out.println(" ---- packge reference");
383    } else if (result.classInfo != null && result.referencedMemberName == null) {
384      // class reference
385      if (result.label.length() == 0) {
386        result.label = result.classInfo.name();
387      }
388      setHref(result, result.classInfo, null);
389      if (DBG) System.out.println(" ---- class reference");
390    } else if (result.memberInfo != null) {
391      // member reference
392      ClassInfo containing = result.memberInfo.containingClass();
393      if (result.memberInfo.isExecutable()) {
394        if (result.referencedMemberName.indexOf('(') < 0) {
395          result.referencedMemberName += method.flatSignature();
396        }
397      }
398      if (result.label.length() == 0) {
399        result.label = result.referencedMemberName;
400      }
401      setHref(result, containing, result.memberInfo.anchor());
402      if (DBG) System.out.println(" ---- member reference");
403    }
404    if (DBG) System.out.println("  --- href = '" + result.href + "'");
405
406    if (result.href == null && !skipHref) {
407      if (printOnErrors && (base == null || base.checkLevel())) {
408        Errors.error(Errors.UNRESOLVED_LINK, pos, "Unresolved link/see tag \"" + text.trim()
409            + "\" in " + ((base != null) ? base.qualifiedName() : "[null]"));
410      }
411      result.makeError();
412    } else if (result.memberInfo != null && !result.memberInfo.checkLevel()) {
413      if (printOnErrors && (base == null || base.checkLevel())) {
414        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden member: " + text.trim());
415        result.href = null;
416      }
417      result.kind = "@seeJustLabel";
418    } else if (result.classInfo != null && !result.classInfo.checkLevel()) {
419      if (printOnErrors && (base == null || base.checkLevel())) {
420        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden class: " + text.trim() + " label="
421            + result.label);
422        result.href = null;
423      }
424      result.kind = "@seeJustLabel";
425    } else if (result.packageInfo != null && !result.packageInfo.checkLevel()) {
426      if (printOnErrors && (base == null || base.checkLevel())) {
427        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden package: " + text.trim());
428        result.href = null;
429      }
430      result.kind = "@seeJustLabel";
431    }
432
433    result.good = true;
434
435    return result;
436  }
437
438  public boolean checkLevel() {
439    if (memberInfo != null) {
440      return memberInfo.checkLevel();
441    }
442    if (classInfo != null) {
443      return classInfo.checkLevel();
444    }
445    if (packageInfo != null) {
446      return packageInfo.checkLevel();
447    }
448    return false;
449  }
450
451  /** turn this LinkReference into one with an error message */
452  private void makeError() {
453    // this.href = "ERROR(" + this.text.trim() + ")";
454    this.href = null;
455    if (this.label == null) {
456      this.label = "";
457    }
458    this.label = "ERROR(" + this.label + "/" + text.trim() + ")";
459  }
460
461  static private void setHref(LinkReference reference, ClassInfo info, String member) {
462    String htmlPage = info.htmlPage();
463    if (member != null) {
464      htmlPage = htmlPage + "#" + member;
465    }
466
467    Doclava.federationTagger.tag(info);
468    if (!info.getFederatedReferences().isEmpty()) {
469      FederatedSite site = info.getFederatedReferences().iterator().next();
470      reference.href = site.linkFor(htmlPage);
471      reference.federatedSite = site.name();
472    } else {
473      reference.href = htmlPage;
474    }
475  }
476
477  /** private. **/
478  private LinkReference() {}
479}
480