1/*
2 * Copyright (C) 2011 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.apicheck;
18
19import com.google.doclava.AnnotationInstanceInfo;
20import com.google.doclava.ClassInfo;
21import com.google.doclava.Converter;
22import com.google.doclava.FieldInfo;
23import com.google.doclava.MethodInfo;
24import com.google.doclava.PackageInfo;
25import com.google.doclava.ParameterInfo;
26import com.google.doclava.SourcePositionInfo;
27import com.google.doclava.TypeInfo;
28
29import java.io.IOException;
30import java.io.InputStream;
31import java.util.ArrayList;
32import java.util.LinkedList;
33
34class ApiFile {
35
36  public static ApiInfo parseApi(String filename, InputStream stream) throws ApiParseException {
37    final int CHUNK = 1024*1024;
38    int hint = 0;
39    try {
40      hint = stream.available() + CHUNK;
41    } catch (IOException ex) {
42    }
43    if (hint < CHUNK) {
44      hint = CHUNK;
45    }
46    byte[] buf = new byte[hint];
47    int size = 0;
48
49    try {
50      while (true) {
51        if (size == buf.length) {
52          byte[] tmp = new byte[buf.length+CHUNK];
53          System.arraycopy(buf, 0, tmp, 0, buf.length);
54          buf = tmp;
55        }
56        int amt = stream.read(buf, size, (buf.length-size));
57        if (amt < 0) {
58          break;
59        } else {
60          size += amt;
61        }
62      }
63    } catch (IOException ex) {
64      throw new ApiParseException("Error reading API file", ex);
65    }
66
67    final Tokenizer tokenizer = new Tokenizer(filename, (new String(buf, 0, size)).toCharArray());
68    final ApiInfo api = new ApiInfo();
69
70    while (true) {
71      String token = tokenizer.getToken();
72      if (token == null) {
73        break;
74      }
75      if ("package".equals(token)) {
76        parsePackage(api, tokenizer);
77      } else {
78        throw new ApiParseException("expected package got " + token, tokenizer.getLine());
79      }
80    }
81
82    api.resolveSuperclasses();
83    api.resolveInterfaces();
84
85    return api;
86  }
87
88  private static void parsePackage(ApiInfo api, Tokenizer tokenizer)
89      throws ApiParseException {
90    String token;
91    String name;
92    PackageInfo pkg;
93
94    token = tokenizer.requireToken();
95    assertIdent(tokenizer, token);
96    name = token;
97    pkg = new PackageInfo(name, tokenizer.pos());
98    token = tokenizer.requireToken();
99    if (!"{".equals(token)) {
100      throw new ApiParseException("expected '{' got " + token, tokenizer.getLine());
101    }
102    while (true) {
103      token = tokenizer.requireToken();
104      if ("}".equals(token)) {
105        break;
106      } else {
107        parseClass(api, pkg, tokenizer, token);
108      }
109    }
110    api.addPackage(pkg);
111  }
112
113  private static void parseClass(ApiInfo api, PackageInfo pkg, Tokenizer tokenizer, String token)
114      throws ApiParseException {
115    boolean pub = false;
116    boolean prot = false;
117    boolean pkgpriv = false;
118    boolean stat = false;
119    boolean fin = false;
120    boolean abs = false;
121    boolean dep = false;
122    boolean iface;
123    String name;
124    String qname;
125    String ext = null;
126    ClassInfo cl;
127
128    if ("public".equals(token)) {
129      pub = true;
130      token = tokenizer.requireToken();
131    } else if ("protected".equals(token)) {
132      prot = true;
133      token = tokenizer.requireToken();
134    } else {
135      pkgpriv = true;
136    }
137    if ("static".equals(token)) {
138      stat = true;
139      token = tokenizer.requireToken();
140    }
141    if ("final".equals(token)) {
142      fin = true;
143      token = tokenizer.requireToken();
144    }
145    if ("abstract".equals(token)) {
146      abs = true;
147      token = tokenizer.requireToken();
148    }
149    if ("deprecated".equals(token)) {
150      dep = true;
151      token = tokenizer.requireToken();
152    }
153    if ("class".equals(token)) {
154      iface = false;
155      token = tokenizer.requireToken();
156    } else if ("interface".equals(token)) {
157      iface = true;
158      token = tokenizer.requireToken();
159    } else {
160      throw new ApiParseException("missing class or interface. got: " + token, tokenizer.getLine());
161    }
162    assertIdent(tokenizer, token);
163    name = token;
164    token = tokenizer.requireToken();
165    qname = qualifiedName(pkg.name(), name, null);
166    cl = new ClassInfo(null/*classDoc*/, ""/*rawCommentText*/, tokenizer.pos(), pub, prot,
167        pkgpriv, false/*isPrivate*/, stat, iface, abs, true/*isOrdinaryClass*/,
168        false/*isException*/, false/*isError*/, false/*isEnum*/, false/*isAnnotation*/,
169        fin, false/*isIncluded*/, name, qname, null/*qualifiedTypeName*/, false/*isPrimitive*/);
170    cl.setDeprecated(dep);
171    if ("extends".equals(token)) {
172      token = tokenizer.requireToken();
173      assertIdent(tokenizer, token);
174      ext = token;
175      token = tokenizer.requireToken();
176    }
177    // Resolve superclass after done parsing
178    api.mapClassToSuper(cl, ext);
179    final TypeInfo typeInfo = Converter.obtainTypeFromString(qname) ;
180    cl.setTypeInfo(typeInfo);
181    cl.setAnnotations(new ArrayList<AnnotationInstanceInfo>());
182    if ("implements".equals(token)) {
183      while (true) {
184        token = tokenizer.requireToken();
185        if ("{".equals(token)) {
186          break;
187        } else {
188          /// TODO
189          if (!",".equals(token)) {
190            api.mapClassToInterface(cl, token);
191          }
192        }
193      }
194    }
195    if (!"{".equals(token)) {
196      throw new ApiParseException("expected {", tokenizer.getLine());
197    }
198    token = tokenizer.requireToken();
199    while (true) {
200      if ("}".equals(token)) {
201        break;
202      } else if ("ctor".equals(token)) {
203        token = tokenizer.requireToken();
204        parseConstructor(tokenizer, cl, token);
205      } else if ("method".equals(token)) {
206        token = tokenizer.requireToken();
207        parseMethod(tokenizer, cl, token);
208      } else if ("field".equals(token)) {
209        token = tokenizer.requireToken();
210        parseField(tokenizer, cl, token, false);
211      } else if ("enum_constant".equals(token)) {
212        token = tokenizer.requireToken();
213        parseField(tokenizer, cl, token, true);
214      } else {
215        throw new ApiParseException("expected ctor, enum_constant, field or method", tokenizer.getLine());
216      }
217      token = tokenizer.requireToken();
218    }
219    pkg.addClass(cl);
220  }
221
222  private static void parseConstructor(Tokenizer tokenizer, ClassInfo cl, String token)
223      throws ApiParseException {
224    boolean pub = false;
225    boolean prot = false;
226    boolean pkgpriv = false;
227    boolean dep = false;
228    String name;
229    MethodInfo method;
230
231    if ("public".equals(token)) {
232      pub = true;
233      token = tokenizer.requireToken();
234    } else if ("protected".equals(token)) {
235      prot = true;
236      token = tokenizer.requireToken();
237    } else {
238      pkgpriv = true;
239    }
240    if ("deprecated".equals(token)) {
241      dep = true;
242      token = tokenizer.requireToken();
243    }
244    assertIdent(tokenizer, token);
245    name = token;
246    token = tokenizer.requireToken();
247    if (!"(".equals(token)) {
248      throw new ApiParseException("expected (", tokenizer.getLine());
249    }
250    //method = new MethodInfo(name, cl.qualifiedName(), false/*static*/, false/*final*/, dep,
251    //    pub ? "public" : "protected", tokenizer.pos(), cl);
252    method = new MethodInfo(""/*rawCommentText*/, new ArrayList<TypeInfo>()/*typeParameters*/,
253        name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, false/*isFinal*/,
254        false/*isStatic*/, false/*isSynthetic*/, false/*isAbstract*/, false/*isSynthetic*/,
255        false/*isNative*/,
256        false /*isAnnotationElement*/, "constructor", null/*flatSignature*/,
257        null/*overriddenMethod*/, cl.asTypeInfo(), new ArrayList<ParameterInfo>(),
258        new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
259        new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
260    method.setDeprecated(dep);
261    token = tokenizer.requireToken();
262    parseParameterList(tokenizer, method, token);
263    token = tokenizer.requireToken();
264    if ("throws".equals(token)) {
265      token = parseThrows(tokenizer, method);
266    }
267    if (!";".equals(token)) {
268      throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
269    }
270    cl.addConstructor(method);
271  }
272
273  private static void parseMethod(Tokenizer tokenizer, ClassInfo cl, String token)
274      throws ApiParseException {
275    boolean pub = false;
276    boolean prot = false;
277    boolean pkgpriv = false;
278    boolean stat = false;
279    boolean fin = false;
280    boolean abs = false;
281    boolean dep = false;
282    boolean syn = false;
283    String type;
284    String name;
285    String ext = null;
286    MethodInfo method;
287
288    if ("public".equals(token)) {
289      pub = true;
290      token = tokenizer.requireToken();
291    } else if ("protected".equals(token)) {
292      prot = true;
293      token = tokenizer.requireToken();
294    } else {
295      pkgpriv = true;
296    }
297    if ("static".equals(token)) {
298      stat = true;
299      token = tokenizer.requireToken();
300    }
301    if ("final".equals(token)) {
302      fin = true;
303      token = tokenizer.requireToken();
304    }
305    if ("abstract".equals(token)) {
306      abs = true;
307      token = tokenizer.requireToken();
308    }
309    if ("deprecated".equals(token)) {
310      dep = true;
311      token = tokenizer.requireToken();
312    }
313    if ("synchronized".equals(token)) {
314      syn = true;
315      token = tokenizer.requireToken();
316    }
317    assertIdent(tokenizer, token);
318    type = token;
319    token = tokenizer.requireToken();
320    assertIdent(tokenizer, token);
321    name = token;
322    method = new MethodInfo(""/*rawCommentText*/, new ArrayList<TypeInfo>()/*typeParameters*/,
323        name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin,
324        stat, false/*isSynthetic*/, abs/*isAbstract*/, syn, false/*isNative*/,
325        false /*isAnnotationElement*/, "method", null/*flatSignature*/, null/*overriddenMethod*/,
326        Converter.obtainTypeFromString(type), new ArrayList<ParameterInfo>(),
327        new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
328        new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
329    method.setDeprecated(dep);
330    token = tokenizer.requireToken();
331    if (!"(".equals(token)) {
332      throw new ApiParseException("expected (", tokenizer.getLine());
333    }
334    token = tokenizer.requireToken();
335    parseParameterList(tokenizer, method, token);
336    token = tokenizer.requireToken();
337    if ("throws".equals(token)) {
338      token = parseThrows(tokenizer, method);
339    }
340    if (!";".equals(token)) {
341      throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
342    }
343    cl.addMethod(method);
344  }
345
346  private static void parseField(Tokenizer tokenizer, ClassInfo cl, String token, boolean isEnum)
347      throws ApiParseException {
348    boolean pub = false;
349    boolean prot = false;
350    boolean pkgpriv = false;
351    boolean stat = false;
352    boolean fin = false;
353    boolean dep = false;
354    boolean trans = false;
355    boolean vol = false;
356    String type;
357    String name;
358    String val = null;
359    Object v;
360    FieldInfo field;
361
362    if ("public".equals(token)) {
363      pub = true;
364      token = tokenizer.requireToken();
365    } else if ("protected".equals(token)) {
366      prot = true;
367      token = tokenizer.requireToken();
368    } else {
369      pkgpriv = true;
370    }
371    if ("static".equals(token)) {
372      stat = true;
373      token = tokenizer.requireToken();
374    }
375    if ("final".equals(token)) {
376      fin = true;
377      token = tokenizer.requireToken();
378    }
379    if ("deprecated".equals(token)) {
380      dep = true;
381      token = tokenizer.requireToken();
382    }
383    if ("transient".equals(token)) {
384      trans = true;
385      token = tokenizer.requireToken();
386    }
387    if ("volatile".equals(token)) {
388      vol = true;
389      token = tokenizer.requireToken();
390    }
391    assertIdent(tokenizer, token);
392    type = token;
393    token = tokenizer.requireToken();
394    assertIdent(tokenizer, token);
395    name = token;
396    token = tokenizer.requireToken();
397    if ("=".equals(token)) {
398      token = tokenizer.requireToken(false);
399      val = token;
400      token = tokenizer.requireToken();
401    }
402    if (!";".equals(token)) {
403      throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
404    }
405    try {
406      v = parseValue(type, val);
407    } catch (ApiParseException ex) {
408      ex.line = tokenizer.getLine();
409      throw ex;
410    }
411    field = new FieldInfo(name, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin, stat,
412        trans, vol, false, Converter.obtainTypeFromString(type), "", v, tokenizer.pos(),
413        new ArrayList<AnnotationInstanceInfo>());
414    field.setDeprecated(dep);
415    if (isEnum) {
416      cl.addEnumConstant(field);
417    } else {
418      cl.addField(field);
419    }
420  }
421
422  public static Object parseValue(String type, String val) throws ApiParseException {
423    if (val != null) {
424      if ("boolean".equals(type)) {
425        return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE;
426      } else if ("byte".equals(type)) {
427        return Integer.valueOf(val);
428      } else if ("short".equals(type)) {
429        return Integer.valueOf(val);
430      } else if ("int".equals(type)) {
431        return Integer.valueOf(val);
432      } else if ("long".equals(type)) {
433        return Long.valueOf(val.substring(0, val.length()-1));
434      } else if ("float".equals(type)) {
435        if ("(1.0f/0.0f)".equals(val) || "(1.0f / 0.0f)".equals(val)) {
436          return Float.POSITIVE_INFINITY;
437        } else if ("(-1.0f/0.0f)".equals(val) || "(-1.0f / 0.0f)".equals(val)) {
438          return Float.NEGATIVE_INFINITY;
439        } else if ("(0.0f/0.0f)".equals(val) || "(0.0f / 0.0f)".equals(val)) {
440          return Float.NaN;
441        } else {
442          return Float.valueOf(val);
443        }
444      } else if ("double".equals(type)) {
445        if ("(1.0/0.0)".equals(val) || "(1.0 / 0.0)".equals(val)) {
446          return Double.POSITIVE_INFINITY;
447        } else if ("(-1.0/0.0)".equals(val) || "(-1.0 / 0.0)".equals(val)) {
448          return Double.NEGATIVE_INFINITY;
449        } else if ("(0.0/0.0)".equals(val) || "(0.0 / 0.0)".equals(val)) {
450          return Double.NaN;
451        } else {
452          return Double.valueOf(val);
453        }
454      } else if ("char".equals(type)) {
455        return new Integer((char)Integer.parseInt(val));
456      } else if ("java.lang.String".equals(type)) {
457        if ("null".equals(val)) {
458          return null;
459        } else {
460          return FieldInfo.javaUnescapeString(val.substring(1, val.length()-1));
461        }
462      }
463    }
464    if ("null".equals(val)) {
465      return null;
466    } else {
467      return val;
468    }
469  }
470
471  private static void parseParameterList(Tokenizer tokenizer, AbstractMethodInfo method,
472      String token) throws ApiParseException {
473    while (true) {
474      if (")".equals(token)) {
475        return;
476      }
477
478      String type = token;
479      String name = null;
480      token = tokenizer.requireToken();
481      if (isIdent(token)) {
482        name = token;
483        token = tokenizer.requireToken();
484      }
485      if (",".equals(token)) {
486        token = tokenizer.requireToken();
487      } else if (")".equals(token)) {
488      } else {
489        throw new ApiParseException("expected , found " + token, tokenizer.getLine());
490      }
491      method.addParameter(new ParameterInfo(name, type,
492            Converter.obtainTypeFromString(type),
493            type.endsWith("..."),
494            tokenizer.pos()));
495      if (type.endsWith("...")) {
496        method.setVarargs(true);
497      }
498    }
499  }
500
501  private static String parseThrows(Tokenizer tokenizer, AbstractMethodInfo method)
502      throws ApiParseException {
503    String token = tokenizer.requireToken();
504    boolean comma = true;
505    while (true) {
506      if (";".equals(token)) {
507        return token;
508      } else if (",".equals(token)) {
509        if (comma) {
510          throw new ApiParseException("Expected exception, got ','", tokenizer.getLine());
511        }
512        comma = true;
513      } else {
514        if (!comma) {
515          throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer.getLine());
516        }
517        comma = false;
518        method.addException(token);
519      }
520      token = tokenizer.requireToken();
521    }
522  }
523
524  private static String qualifiedName(String pkg, String className, ClassInfo parent) {
525    String parentQName = (parent != null) ? (parent.qualifiedName() + ".") : "";
526    return pkg + "." + parentQName + className;
527  }
528
529  public static boolean isIdent(String token) {
530    return isident(token.charAt(0));
531  }
532
533  public static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException {
534    if (!isident(token.charAt(0))) {
535      throw new ApiParseException("Expected identifier: " + token, tokenizer.getLine());
536    }
537  }
538
539  static class Tokenizer {
540    char[] mBuf;
541    String mFilename;
542    int mPos;
543    int mLine = 1;
544    Tokenizer(String filename, char[] buf) {
545      mFilename = filename;
546      mBuf = buf;
547    }
548
549    public SourcePositionInfo pos() {
550      return new SourcePositionInfo(mFilename, mLine, 0);
551    }
552
553    public int getLine() {
554      return mLine;
555    }
556
557    boolean eatWhitespace() {
558      boolean ate = false;
559      while (mPos < mBuf.length && isspace(mBuf[mPos])) {
560        if (mBuf[mPos] == '\n') {
561          mLine++;
562        }
563        mPos++;
564        ate = true;
565      }
566      return ate;
567    }
568
569    boolean eatComment() {
570      if (mPos+1 < mBuf.length) {
571        if (mBuf[mPos] == '/' && mBuf[mPos+1] == '/') {
572          mPos += 2;
573          while (mPos < mBuf.length && !isnewline(mBuf[mPos])) {
574            mPos++;
575          }
576          return true;
577        }
578      }
579      return false;
580    }
581
582    void eatWhitespaceAndComments() {
583      while (eatWhitespace() || eatComment()) {
584      }
585    }
586
587    public String requireToken() throws ApiParseException {
588      return requireToken(true);
589    }
590
591    public String requireToken(boolean parenIsSep) throws ApiParseException {
592      final String token = getToken(parenIsSep);
593      if (token != null) {
594        return token;
595      } else {
596        throw new ApiParseException("Unexpected end of file", mLine);
597      }
598    }
599
600    public String getToken() throws ApiParseException {
601      return getToken(true);
602    }
603
604    public String getToken(boolean parenIsSep) throws ApiParseException {
605      eatWhitespaceAndComments();
606      if (mPos >= mBuf.length) {
607        return null;
608      }
609      final int line = mLine;
610      final char c = mBuf[mPos];
611      final int start = mPos;
612      mPos++;
613      if (c == '"') {
614        final int STATE_BEGIN = 0;
615        final int STATE_ESCAPE = 1;
616        int state = STATE_BEGIN;
617        while (true) {
618          if (mPos >= mBuf.length) {
619            throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine);
620          }
621          final char k = mBuf[mPos];
622          if (k == '\n' || k == '\r') {
623            throw new ApiParseException("Unexpected newline for \" starting at " + line, mLine);
624          }
625          mPos++;
626          switch (state) {
627            case STATE_BEGIN:
628              switch (k) {
629                case '\\':
630                  state = STATE_ESCAPE;
631                  mPos++;
632                  break;
633                case '"':
634                  return new String(mBuf, start, mPos-start);
635              }
636            case STATE_ESCAPE:
637              state = STATE_BEGIN;
638              break;
639          }
640        }
641      } else if (issep(c, parenIsSep)) {
642        return "" + c;
643      } else {
644        int genericDepth = 0;
645        do {
646          while (mPos < mBuf.length && !isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) {
647            mPos++;
648          }
649          if (mPos < mBuf.length) {
650            if (mBuf[mPos] == '<') {
651              genericDepth++;
652              mPos++;
653            } else if (mBuf[mPos] == '>') {
654              genericDepth--;
655              mPos++;
656            } else if (genericDepth != 0) {
657              mPos++;
658            }
659          }
660        } while (mPos < mBuf.length
661            && ((!isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) || genericDepth != 0));
662        if (mPos >= mBuf.length) {
663          throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine);
664        }
665        return new String(mBuf, start, mPos-start);
666      }
667    }
668  }
669
670  static boolean isspace(char c) {
671    return c == ' ' || c == '\t' || c == '\n' || c == '\r';
672  }
673
674  static boolean isnewline(char c) {
675    return c == '\n' || c == '\r';
676  }
677
678  static boolean issep(char c, boolean parenIsSep) {
679    if (parenIsSep) {
680      if (c == '(' || c == ')') {
681        return true;
682      }
683    }
684    return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>';
685  }
686
687  static boolean isident(char c) {
688    if (c == '"' || issep(c, true)) {
689      return false;
690    }
691    return true;
692  }
693}
694
695