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    setFederatedReferences(data, base);
386  }
387
388  @Override
389  public boolean isExecutable() {
390    return false;
391  }
392
393  public boolean isTransient() {
394    return mIsTransient;
395  }
396
397  public boolean isVolatile() {
398    return mIsVolatile;
399  }
400
401  // Check the declared value with a typed comparison, not a string comparison,
402  // to accommodate toolchains with different fp -> string conversions.
403  private boolean valueEquals(FieldInfo other) {
404    if ((mConstantValue == null) != (other.mConstantValue == null)) {
405      return false;
406    }
407
408    // Null values are considered equal
409    if (mConstantValue == null) {
410      return true;
411    }
412
413    return mType.equals(other.mType)
414        && mConstantValue.equals(other.mConstantValue);
415  }
416
417  public boolean isConsistent(FieldInfo fInfo) {
418    boolean consistent = true;
419    if (!mType.equals(fInfo.mType)) {
420      Errors.error(Errors.CHANGED_TYPE, fInfo.position(), "Field " + fInfo.qualifiedName()
421          + " has changed type");
422      consistent = false;
423    } else if (!this.valueEquals(fInfo)) {
424      Errors.error(Errors.CHANGED_VALUE, fInfo.position(), "Field " + fInfo.qualifiedName()
425          + " has changed value from " + mConstantValue + " to " + fInfo.mConstantValue);
426      consistent = false;
427    }
428
429    if (!scope().equals(fInfo.scope())) {
430      Errors.error(Errors.CHANGED_SCOPE, fInfo.position(), "Method " + fInfo.qualifiedName()
431          + " changed scope from " + this.scope() + " to " + fInfo.scope());
432      consistent = false;
433    }
434
435    if (mIsStatic != fInfo.mIsStatic) {
436      Errors.error(Errors.CHANGED_STATIC, fInfo.position(), "Field " + fInfo.qualifiedName()
437          + " has changed 'static' qualifier");
438      consistent = false;
439    }
440
441    if (!mIsFinal && fInfo.mIsFinal) {
442      Errors.error(Errors.ADDED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
443          + " has added 'final' qualifier");
444      consistent = false;
445    } else if (mIsFinal && !fInfo.mIsFinal) {
446      Errors.error(Errors.REMOVED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
447          + " has removed 'final' qualifier");
448      consistent = false;
449    }
450
451    if (mIsTransient != fInfo.mIsTransient) {
452      Errors.error(Errors.CHANGED_TRANSIENT, fInfo.position(), "Field " + fInfo.qualifiedName()
453          + " has changed 'transient' qualifier");
454      consistent = false;
455    }
456
457    if (mIsVolatile != fInfo.mIsVolatile) {
458      Errors.error(Errors.CHANGED_VOLATILE, fInfo.position(), "Field " + fInfo.qualifiedName()
459          + " has changed 'volatile' qualifier");
460      consistent = false;
461    }
462
463    if (isDeprecated() != fInfo.isDeprecated()) {
464      Errors.error(Errors.CHANGED_DEPRECATED, fInfo.position(), "Field " + fInfo.qualifiedName()
465          + " has changed deprecation state");
466      consistent = false;
467    }
468
469    return consistent;
470  }
471
472  public boolean hasValue() {
473      return mHasValue;
474  }
475
476  public void setHasValue(boolean hasValue) {
477      mHasValue = hasValue;
478  }
479
480  boolean mIsTransient;
481  boolean mIsVolatile;
482  boolean mDeprecatedKnown;
483  boolean mIsDeprecated;
484  boolean mHasValue;
485  TypeInfo mType;
486  Object mConstantValue;
487}
488