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