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;
20import com.google.doclava.apicheck.ApiParseException;
21
22import java.util.ArrayList;
23import java.util.Comparator;
24import java.util.HashSet;
25import java.util.Objects;
26
27public class FieldInfo extends MemberInfo {
28  public static final Comparator<FieldInfo> comparator = new Comparator<FieldInfo>() {
29    public int compare(FieldInfo a, FieldInfo b) {
30      return a.name().compareTo(b.name());
31    }
32  };
33
34  public FieldInfo(String name, ClassInfo containingClass, ClassInfo realContainingClass,
35      boolean isPublic, boolean isProtected, boolean isPackagePrivate, boolean isPrivate,
36      boolean isFinal, boolean isStatic, boolean isTransient, boolean isVolatile,
37      boolean isSynthetic, TypeInfo type, String rawCommentText, Object constantValue,
38      SourcePositionInfo position, ArrayList<AnnotationInstanceInfo> annotations) {
39    super(rawCommentText, name, null, containingClass, realContainingClass, isPublic, isProtected,
40          isPackagePrivate, isPrivate, isFinal, isStatic, isSynthetic, chooseKind(isFinal, isStatic, constantValue),
41        position, annotations);
42    mIsTransient = isTransient;
43    mIsVolatile = isVolatile;
44    mType = type;
45    mConstantValue = constantValue;
46  }
47
48  public FieldInfo cloneForClass(ClassInfo newContainingClass) {
49    if (newContainingClass == containingClass()) {
50      return this;
51    }
52    return new FieldInfo(name(), newContainingClass, realContainingClass(), isPublic(),
53        isProtected(), isPackagePrivate(), isPrivate(), isFinal(), isStatic(), isTransient(),
54        isVolatile(), isSynthetic(), mType, getRawCommentText(), mConstantValue, position(),
55        annotations());
56  }
57
58  static String chooseKind(boolean isFinal, boolean isStatic, Object constantValue)
59  {
60    return isConstant(isFinal, isStatic, constantValue) ? "constant" : "field";
61  }
62
63  @Override
64  public String toString() {
65    return this.name();
66  }
67
68  @Override
69  public boolean equals(Object o) {
70    if (this == o) {
71      return true;
72    } else if (o instanceof FieldInfo) {
73      final FieldInfo f = (FieldInfo) o;
74      return mName.equals(f.mName);
75    } else {
76      return false;
77    }
78  }
79
80  @Override
81  public int hashCode() {
82    return mName.hashCode();
83  }
84
85  public String qualifiedName() {
86    String parentQName
87        = (containingClass() != null) ? (containingClass().qualifiedName() + ".") : "";
88    return parentQName + name();
89  }
90
91  public TypeInfo type() {
92    return mType;
93  }
94
95  public HashSet<String> typeVariables() {
96    HashSet<String> result = new HashSet<String>();
97    ClassInfo cl = containingClass();
98    while (cl != null) {
99      ArrayList<TypeInfo> types = cl.asTypeInfo().typeArguments();
100      if (types != null) {
101        TypeInfo.typeVariables(types, result);
102      }
103      cl = cl.containingClass();
104    }
105    return result;
106  }
107
108  static boolean isConstant(boolean isFinal, boolean isStatic, Object constantValue)
109  {
110    /*
111     * Note: There is an ambiguity in the doc API that prevents us
112     * from distinguishing a constant-null from the lack of a
113     * constant at all. We err on the side of treating all null
114     * constantValues as meaning that the field is not a constant,
115     * since having a static final field assigned to null is both
116     * unusual and generally pretty useless.
117     */
118    return isFinal && isStatic && (constantValue != null);
119  }
120
121  public boolean isConstant() {
122    return isConstant(isFinal(), isStatic(), mConstantValue);
123  }
124
125  public TagInfo[] firstSentenceTags() {
126    return comment().briefTags();
127  }
128
129  public TagInfo[] inlineTags() {
130    return comment().tags();
131  }
132
133  public Object constantValue() {
134    return mConstantValue;
135  }
136
137  public String constantLiteralValue() {
138    return constantLiteralValue(mConstantValue);
139  }
140
141  public void setDeprecated(boolean deprecated) {
142    mDeprecatedKnown = true;
143    mIsDeprecated = deprecated;
144  }
145
146  public boolean isDeprecated() {
147    if (!mDeprecatedKnown) {
148      boolean commentDeprecated = comment().isDeprecated();
149      boolean annotationDeprecated = false;
150      for (AnnotationInstanceInfo annotation : annotations()) {
151        if (annotation.type().qualifiedName().equals("java.lang.Deprecated")) {
152          annotationDeprecated = true;
153          break;
154        }
155      }
156
157      // Check to see that the JavaDoc contains @deprecated AND the method is marked as @Deprecated.
158      // Otherwise, warn.
159      // Note: We only do this for "included" classes (i.e. those we have source code for); we do
160      // not have comments for classes from .class files but we do know whether a field is marked
161      // as @Deprecated.
162      if (mContainingClass.isIncluded() && !isHiddenOrRemoved()
163          && commentDeprecated != annotationDeprecated) {
164        Errors.error(Errors.DEPRECATION_MISMATCH, position(), "Field "
165            + mContainingClass.qualifiedName() + "." + name()
166            + ": @Deprecated annotation (" + (annotationDeprecated ? "" : "not ")
167            + "present) and @deprecated doc tag (" + (commentDeprecated ? "" : "not ")
168            + "present) do not match");
169      }
170
171      mIsDeprecated = commentDeprecated | annotationDeprecated;
172      mDeprecatedKnown = true;
173    }
174    return mIsDeprecated;
175  }
176
177  public static String constantLiteralValue(Object val) {
178    String str = null;
179    if (val != null) {
180      if (val instanceof Boolean || val instanceof Byte || val instanceof Short
181          || val instanceof Integer) {
182        str = val.toString();
183      }
184      // catch all special values
185      else if (val instanceof Double) {
186        str = canonicalizeFloatingPoint(val.toString(), "");
187      } else if (val instanceof Float) {
188        str = canonicalizeFloatingPoint(val.toString(), "f");
189      } else if (val instanceof Long) {
190        str = val.toString() + "L";
191      } else if (val instanceof Character) {
192        str = String.format("\'\\u%04x\'", val);
193        System.out.println("str=" + str);
194      } else if (val instanceof String) {
195        str = "\"" + javaEscapeString((String) val) + "\"";
196      } else {
197        str = "<<<<" + val.toString() + ">>>>";
198      }
199    }
200    if (str == null) {
201      str = "null";
202    }
203    return str;
204  }
205
206  /**
207   * Returns a canonical string representation of a floating point
208   * number. The representation is suitable for use as Java source
209   * code. This method also addresses bug #4428022 in the Sun JDK.
210   */
211  private static String canonicalizeFloatingPoint(String val, String suffix) {
212    if (val.equals("Infinity")) {
213      return "(1.0" + suffix + "/0.0" + suffix + ")";
214    } else if (val.equals("-Infinity")) {
215      return "(-1.0" + suffix + "/0.0" + suffix + ")";
216    } else if (val.equals("NaN")) {
217      return "(0.0" + suffix + "/0.0" + suffix + ")";
218    }
219
220    String str = val.toString();
221    if (str.indexOf('E') != -1) {
222      return str + suffix;
223    }
224
225    // 1.0 is the only case where a trailing "0" is allowed.
226    // 1.00 is canonicalized as 1.0.
227    int i = str.length() - 1;
228    int d = str.indexOf('.');
229    while (i >= d + 2 && str.charAt(i) == '0') {
230      str = str.substring(0, i--);
231    }
232    return str + suffix;
233  }
234
235  public static String javaEscapeString(String str) {
236    String result = "";
237    final int N = str.length();
238    for (int i = 0; i < N; i++) {
239      char c = str.charAt(i);
240      if (c == '\\') {
241        result += "\\\\";
242      } else if (c == '\t') {
243        result += "\\t";
244      } else if (c == '\b') {
245        result += "\\b";
246      } else if (c == '\r') {
247        result += "\\r";
248      } else if (c == '\n') {
249        result += "\\n";
250      } else if (c == '\f') {
251        result += "\\f";
252      } else if (c == '\'') {
253        result += "\\'";
254      } else if (c == '\"') {
255        result += "\\\"";
256      } else if (c >= ' ' && c <= '~') {
257        result += c;
258      } else {
259        result += String.format("\\u%04x", new Integer((int) c));
260      }
261    }
262    return result;
263  }
264
265  public static String javaUnescapeString(String str) throws ApiParseException {
266    final int N = str.length();
267    check: {
268      for (int i=0; i<N; i++) {
269        final char c = str.charAt(i);
270        if (c == '\\') {
271          break check;
272        }
273      }
274      return str;
275    }
276
277    final StringBuilder buf = new StringBuilder(str.length());
278    char escaped = 0;
279    final int START = 0;
280    final int CHAR1 = 1;
281    final int CHAR2 = 2;
282    final int CHAR3 = 3;
283    final int CHAR4 = 4;
284    final int ESCAPE = 5;
285    int state = START;
286
287    for (int i=0; i<N; i++) {
288      final char c = str.charAt(i);
289      switch (state) {
290        case START:
291          if (c == '\\') {
292            state = ESCAPE;
293          } else {
294            buf.append(c);
295          }
296          break;
297        case ESCAPE:
298          switch (c) {
299            case '\\':
300              buf.append('\\');
301              state = START;
302              break;
303            case 't':
304              buf.append('\t');
305              state = START;
306              break;
307            case 'b':
308              buf.append('\b');
309              state = START;
310              break;
311            case 'r':
312              buf.append('\r');
313              state = START;
314              break;
315            case 'n':
316              buf.append('\n');
317              state = START;
318              break;
319            case 'f':
320              buf.append('\f');
321              state = START;
322              break;
323            case '\'':
324              buf.append('\'');
325              state = START;
326              break;
327            case '\"':
328              buf.append('\"');
329              state = START;
330              break;
331            case 'u':
332              state = CHAR1;
333              escaped = 0;
334              break;
335          }
336          break;
337        case CHAR1:
338        case CHAR2:
339        case CHAR3:
340        case CHAR4:
341          escaped <<= 4;
342          if (c >= '0' && c <= '9') {
343            escaped |= c - '0';
344          } else if (c >= 'a' && c <= 'f') {
345            escaped |= 10 + (c - 'a');
346          } else if (c >= 'A' && c <= 'F') {
347            escaped |= 10 + (c - 'A');
348          } else {
349            throw new ApiParseException("bad escape sequence: '" + c + "' at pos " + i + " in: \""
350                + str + "\"");
351          }
352          if (state == CHAR4) {
353            buf.append(escaped);
354            state = START;
355          } else {
356            state++;
357          }
358          break;
359      }
360    }
361    if (state != START) {
362      throw new ApiParseException("unfinished escape sequence: " + str);
363    }
364    return buf.toString();
365  }
366
367  public void makeHDF(Data data, String base) {
368    data.setValue(base + ".kind", kind());
369    type().makeHDF(data, base + ".type");
370    data.setValue(base + ".name", name());
371    data.setValue(base + ".href", htmlPage());
372    data.setValue(base + ".anchor", anchor());
373    TagInfo.makeHDF(data, base + ".shortDescr", firstSentenceTags());
374    TagInfo.makeHDF(data, base + ".descr", inlineTags());
375    TagInfo.makeHDF(data, base + ".descrAux", Doclava.auxSource.fieldAuxTags(this));
376    TagInfo.makeHDF(data, base + ".deprecated", comment().deprecatedTags());
377    TagInfo.makeHDF(data, base + ".seeAlso", comment().seeTags());
378    data.setValue(base + ".since", getSince());
379    if (isDeprecated()) {
380      data.setValue(base + ".deprecatedsince", getDeprecatedSince());
381    }
382    data.setValue(base + ".final", isFinal() ? "final" : "");
383    data.setValue(base + ".static", isStatic() ? "static" : "");
384    if (isPublic()) {
385      data.setValue(base + ".scope", "public");
386    } else if (isProtected()) {
387      data.setValue(base + ".scope", "protected");
388    } else if (isPackagePrivate()) {
389      data.setValue(base + ".scope", "");
390    } else if (isPrivate()) {
391      data.setValue(base + ".scope", "private");
392    }
393    Object val = mConstantValue;
394    if (val != null) {
395      String dec = null;
396      String hex = null;
397      String str = null;
398
399      if (val instanceof Boolean) {
400        str = ((Boolean) val).toString();
401      } else if (val instanceof Byte) {
402        dec = String.format("%d", val);
403        hex = String.format("0x%02x", val);
404      } else if (val instanceof Character) {
405        dec = String.format("\'%c\'", val);
406        hex = String.format("0x%04x", val);
407      } else if (val instanceof Double) {
408        str = ((Double) val).toString();
409      } else if (val instanceof Float) {
410        str = ((Float) val).toString();
411      } else if (val instanceof Integer) {
412        dec = String.format("%d", val);
413        hex = String.format("0x%08x", val);
414      } else if (val instanceof Long) {
415        dec = String.format("%d", val);
416        hex = String.format("0x%016x", val);
417      } else if (val instanceof Short) {
418        dec = String.format("%d", val);
419        hex = String.format("0x%04x", val);
420      } else if (val instanceof String) {
421        str = "\"" + ((String) val) + "\"";
422      } else {
423        str = "";
424      }
425
426      if (dec != null && hex != null) {
427        data.setValue(base + ".constantValue.dec", Doclava.escape(dec));
428        data.setValue(base + ".constantValue.hex", Doclava.escape(hex));
429      } else {
430        data.setValue(base + ".constantValue.str", Doclava.escape(str));
431        data.setValue(base + ".constantValue.isString", "1");
432      }
433    }
434
435    AnnotationInstanceInfo.makeLinkListHDF(
436      data,
437      base + ".showAnnotations",
438      showAnnotations().toArray(new AnnotationInstanceInfo[showAnnotations().size()]));
439
440    setFederatedReferences(data, base);
441
442    Doclava.linter.lintField(this);
443  }
444
445  @Override
446  public boolean isExecutable() {
447    return false;
448  }
449
450  public boolean isTransient() {
451    return mIsTransient;
452  }
453
454  public boolean isVolatile() {
455    return mIsVolatile;
456  }
457
458  // Check the declared value with a typed comparison, not a string comparison,
459  // to accommodate toolchains with different fp -> string conversions.
460  private boolean valueEquals(FieldInfo other) {
461    if ((mConstantValue == null) != (other.mConstantValue == null)) {
462      return false;
463    }
464
465    // Null values are considered equal
466    if (mConstantValue == null) {
467      return true;
468    }
469
470    return mType.equals(other.mType)
471        && mConstantValue.equals(other.mConstantValue);
472  }
473
474  public boolean isConsistent(FieldInfo fInfo) {
475    boolean consistent = true;
476    if (!mType.equals(fInfo.mType)) {
477      Errors.error(Errors.CHANGED_TYPE, fInfo.position(), "Field " + fInfo.qualifiedName()
478          + " has changed type from " + mType + " to " + fInfo.mType);
479      consistent = false;
480    } else if (!this.valueEquals(fInfo)) {
481      Errors.error(Errors.CHANGED_VALUE, fInfo.position(), "Field " + fInfo.qualifiedName()
482          + " has changed value from " + mConstantValue + " to " + fInfo.mConstantValue);
483      consistent = false;
484    }
485
486    if (!scope().equals(fInfo.scope())) {
487      Errors.error(Errors.CHANGED_SCOPE, fInfo.position(), "Method " + fInfo.qualifiedName()
488          + " changed scope from " + this.scope() + " to " + fInfo.scope());
489      consistent = false;
490    }
491
492    if (mIsStatic != fInfo.mIsStatic) {
493      Errors.error(Errors.CHANGED_STATIC, fInfo.position(), "Field " + fInfo.qualifiedName()
494          + " has changed 'static' qualifier");
495      consistent = false;
496    }
497
498    if (!mIsFinal && fInfo.mIsFinal) {
499      Errors.error(Errors.ADDED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
500          + " has added 'final' qualifier");
501      consistent = false;
502    } else if (mIsFinal && !fInfo.mIsFinal) {
503      Errors.error(Errors.REMOVED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
504          + " has removed 'final' qualifier");
505      consistent = false;
506    }
507
508    if (mIsTransient != fInfo.mIsTransient) {
509      Errors.error(Errors.CHANGED_TRANSIENT, fInfo.position(), "Field " + fInfo.qualifiedName()
510          + " has changed 'transient' qualifier");
511      consistent = false;
512    }
513
514    if (mIsVolatile != fInfo.mIsVolatile) {
515      Errors.error(Errors.CHANGED_VOLATILE, fInfo.position(), "Field " + fInfo.qualifiedName()
516          + " has changed 'volatile' qualifier");
517      consistent = false;
518    }
519
520    if (isDeprecated() != fInfo.isDeprecated()) {
521      Errors.error(Errors.CHANGED_DEPRECATED, fInfo.position(), "Field " + fInfo.qualifiedName()
522          + " has changed deprecation state " + isDeprecated() + " --> " + fInfo.isDeprecated());
523      consistent = false;
524    }
525
526    return consistent;
527  }
528
529  public boolean hasValue() {
530      return mHasValue;
531  }
532
533  public void setHasValue(boolean hasValue) {
534      mHasValue = hasValue;
535  }
536
537  boolean mIsTransient;
538  boolean mIsVolatile;
539  boolean mDeprecatedKnown;
540  boolean mIsDeprecated;
541  boolean mHasValue;
542  TypeInfo mType;
543  Object mConstantValue;
544}
545