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        try {
255          if (DBG) System.out.println("-- cl non-null");
256          result.classInfo = cl.extendedFindClass(ref);
257          if (result.classInfo == null) {
258            if (DBG) System.out.println("-- cl.extendedFindClass was null");
259            result.classInfo = cl.findClass(ref);
260          }
261          if (result.classInfo == null) {
262            if (DBG) System.out.println("-- cl.findClass was null");
263            result.classInfo = cl.findInnerClass(ref);
264            if (DBG) if (result.classInfo == null) System.out.println("-- cl.findInnerClass was null");
265          }
266        } catch (RuntimeException e) {
267          throw new RuntimeException("Failed to resolve class at " + pos, e);
268        }
269      }
270      if (result.classInfo == null) {
271        if (DBG) System.out.println("-- hitting up the Converter.obtainclass");
272        result.classInfo = Converter.obtainClass(ref);
273      }
274      if (result.classInfo == null) {
275        if (DBG) System.out.println("-- Converter.obtainClass was null");
276        result.packageInfo = Converter.obtainPackage(ref);
277      }
278    }
279
280    if (result.classInfo == null) {
281        if (DBG) System.out.println("-- NO CLASS INFO");
282    } else {
283        Doclava.federationTagger.tag(result.classInfo);
284        for (FederatedSite site : result.classInfo.getFederatedReferences()) {
285          if (DBG) System.out.println("-- reg link = " + result.classInfo.htmlPage());
286          if (DBG) System.out.println("-- fed link = " +
287              site.linkFor(result.classInfo.htmlPage()));
288        }
289    }
290
291    if (result.classInfo != null && mem != null) {
292      // it's either a field or a method, prefer a field
293      if (params == null) {
294        FieldInfo field = result.classInfo.findField(mem);
295        // findField looks in containing classes, so it might actually
296        // be somewhere else; link to where it really is, not what they
297        // typed.
298        if (field != null) {
299          result.classInfo = field.containingClass();
300          result.memberInfo = field;
301        }
302      }
303      if (result.memberInfo == null) {
304        MethodInfo method = result.classInfo.findMethod(mem, params, paramDimensions, varargs);
305        if (method != null) {
306          result.classInfo = method.containingClass();
307          result.memberInfo = method;
308        }
309      }
310    }
311
312    result.referencedMemberName = mem;
313    if (params != null) {
314      result.referencedMemberName = result.referencedMemberName + '(';
315      len = params.length;
316      if (len > 0) {
317        len--;
318        for (int i = 0; i < len; i++) {
319          result.referencedMemberName =
320              result.referencedMemberName + params[i] + paramDimensions[i] + ", ";
321        }
322        result.referencedMemberName =
323            result.referencedMemberName + params[len] + paramDimensions[len];
324      }
325      result.referencedMemberName = result.referencedMemberName + ")";
326    }
327
328    // debugging spew
329    if (false) {
330      result.label = result.label + "/" + ref + "/" + mem + '/';
331      if (params != null) {
332        for (int i = 0; i < params.length; i++) {
333          result.label += params[i] + "|";
334        }
335      }
336
337      FieldInfo f = (result.memberInfo instanceof FieldInfo) ? (FieldInfo) result.memberInfo : null;
338      MethodInfo m =
339          (result.memberInfo instanceof MethodInfo) ? (MethodInfo) result.memberInfo : null;
340      result.label =
341          result.label + "/package="
342              + (result.packageInfo != null ? result.packageInfo.name() : "") + "/class="
343              + (result.classInfo != null ? result.classInfo.qualifiedName() : "") + "/field="
344              + (f != null ? f.name() : "") + "/method=" + (m != null ? m.name() : "");
345
346    }
347
348    MethodInfo method = null;
349    boolean skipHref = false;
350
351    if (result.memberInfo != null && result.memberInfo.isExecutable()) {
352      method = (MethodInfo) result.memberInfo;
353    }
354
355    if (DBG) System.out.println("----- label = " + result.label + ", text = '" + text + "'");
356    if (text.startsWith("\"")) {
357      // literal quoted reference (e.g., a book title)
358      Matcher matcher = QUOTE_PATTERN.matcher(text);
359      if (!matcher.matches()) {
360        Errors.error(Errors.UNRESOLVED_LINK, pos, "unbalanced quoted link/see tag: " + text.trim());
361        result.makeError();
362        return result;
363      }
364      skipHref = true;
365      result.label = matcher.group(1);
366      result.kind = "@seeJustLabel";
367      if (DBG) System.out.println(" ---- literal quoted reference");
368    } else if (text.startsWith("<")) {
369      // explicit "<a href" form
370      Matcher matcher = HREF_PATTERN.matcher(text);
371      if (!matcher.matches()) {
372        Errors.error(Errors.UNRESOLVED_LINK, pos, "invalid <a> link/see tag: " + text.trim());
373        result.makeError();
374        return result;
375      }
376      result.href = matcher.group(1);
377      result.label = matcher.group(2);
378      result.kind = "@seeHref";
379      if (DBG) System.out.println(" ---- explicit href reference");
380    } else if (result.packageInfo != null) {
381      result.href = result.packageInfo.htmlPage();
382      if (result.label.length() == 0) {
383        result.href = result.packageInfo.htmlPage();
384        result.label = result.packageInfo.name();
385      }
386      if (DBG) System.out.println(" ---- packge reference");
387    } else if (result.classInfo != null && result.referencedMemberName == null) {
388      // class reference
389      if (result.label.length() == 0) {
390        result.label = result.classInfo.name();
391      }
392      setHref(result, result.classInfo, null);
393      if (DBG) System.out.println(" ---- class reference");
394    } else if (result.memberInfo != null) {
395      // member reference
396      ClassInfo containing = result.memberInfo.containingClass();
397      if (result.memberInfo.isExecutable()) {
398        if (result.referencedMemberName.indexOf('(') < 0) {
399          result.referencedMemberName += method.flatSignature();
400        }
401      }
402      if (result.label.length() == 0) {
403        result.label = result.referencedMemberName;
404      }
405      setHref(result, containing, result.memberInfo.anchor());
406      if (DBG) System.out.println(" ---- member reference");
407    }
408    if (DBG) System.out.println("  --- href = '" + result.href + "'");
409
410    if (result.href == null && !skipHref) {
411      if (printOnErrors && (base == null || base.checkLevel())) {
412        Errors.error(Errors.UNRESOLVED_LINK, pos, "Unresolved link/see tag \"" + text.trim()
413            + "\" in " + ((base != null) ? base.qualifiedName() : "[null]"));
414      }
415      result.makeError();
416    } else if (result.memberInfo != null && !result.memberInfo.checkLevel()) {
417      if (printOnErrors && (base == null || base.checkLevel())) {
418        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden member: " + text.trim());
419        result.href = null;
420      }
421      result.kind = "@seeJustLabel";
422    } else if (result.classInfo != null && !result.classInfo.checkLevel()) {
423      if (printOnErrors && (base == null || base.checkLevel())) {
424        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden class: " + text.trim() + " label="
425            + result.label);
426        result.href = null;
427      }
428      result.kind = "@seeJustLabel";
429    } else if (result.packageInfo != null && !result.packageInfo.checkLevel()) {
430      if (printOnErrors && (base == null || base.checkLevel())) {
431        Errors.error(Errors.HIDDEN_LINK, pos, "Link to hidden package: " + text.trim());
432        result.href = null;
433      }
434      result.kind = "@seeJustLabel";
435    }
436
437    result.good = true;
438
439    return result;
440  }
441
442  public boolean checkLevel() {
443    if (memberInfo != null) {
444      return memberInfo.checkLevel();
445    }
446    if (classInfo != null) {
447      return classInfo.checkLevel();
448    }
449    if (packageInfo != null) {
450      return packageInfo.checkLevel();
451    }
452    return false;
453  }
454
455  /** turn this LinkReference into one with an error message */
456  private void makeError() {
457    // this.href = "ERROR(" + this.text.trim() + ")";
458    this.href = null;
459    if (this.label == null) {
460      this.label = "";
461    }
462    this.label = "ERROR(" + this.label + "/" + text.trim() + ")";
463  }
464
465  static private void setHref(LinkReference reference, ClassInfo info, String member) {
466    String htmlPage = info.htmlPage();
467    if (member != null) {
468      htmlPage = htmlPage + "#" + member;
469    }
470
471    Doclava.federationTagger.tag(info);
472    if (!info.getFederatedReferences().isEmpty()) {
473      FederatedSite site = info.getFederatedReferences().iterator().next();
474      reference.href = site.linkFor(htmlPage);
475      reference.federatedSite = site.name();
476    } else {
477      reference.href = htmlPage;
478    }
479  }
480
481  /** private. **/
482  private LinkReference() {}
483}
484