Normalize image paths pulled from img with itemprop=image b/31772103 am: ee1c8fa0ae
am: fc9cad27e5

Change-Id: Ia6ad20f1b5f9b682892acea7f33195f210569d98
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
index 8f0199e..7b50938 100644
--- a/src/com/google/doclava/ClassInfo.java
+++ b/src/com/google/doclava/ClassInfo.java
@@ -312,6 +312,19 @@
     return result;
   }
 
+  public TypeInfo getTypeParameter(String qualifiedTypeName) {
+      List<TypeInfo> parameters = mTypeInfo.typeArguments();
+      if (parameters == null) {
+          return null;
+      }
+      for (TypeInfo parameter : parameters) {
+          if (parameter.qualifiedTypeName().equals(qualifiedTypeName)) {
+              return parameter;
+          }
+      }
+      return null;
+  }
+
   /**
    * List of only direct interface's classes, without worrying about type param mapping.
    * This can't be lazy loaded, because its overloads depend on changing type parameters
@@ -2232,6 +2245,17 @@
       }
     }
 
+    if (hasTypeParameters() && cl.hasTypeParameters()) {
+      ArrayList<TypeInfo> oldParams = typeParameters();
+      ArrayList<TypeInfo> newParams = cl.typeParameters();
+      if (oldParams.size() != newParams.size()) {
+        consistent = false;
+        Errors.error(Errors.CHANGED_TYPE, cl.position(), "Class " + qualifiedName()
+            + " changed number of type parameters from " + oldParams.size()
+            + " to " + newParams.size());
+      }
+    }
+
     return consistent;
   }
 
@@ -2284,6 +2308,20 @@
       return mTypeInfo;
   }
 
+  public boolean hasTypeParameters() {
+      if (mTypeInfo != null && mTypeInfo.typeArguments() != null) {
+          return !mTypeInfo.typeArguments().isEmpty();
+      }
+      return false;
+  }
+
+  public ArrayList<TypeInfo> typeParameters() {
+      if (hasTypeParameters()) {
+          return mTypeInfo.typeArguments();
+      }
+      return null;
+  }
+
   public void addInnerClass(ClassInfo innerClass) {
       if (mInnerClasses == null) {
           mInnerClasses = new ArrayList<ClassInfo>();
diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java
index 299043d..28f1899 100644
--- a/src/com/google/doclava/MethodInfo.java
+++ b/src/com/google/doclava/MethodInfo.java
@@ -237,6 +237,10 @@
     return mTypeParameters;
   }
 
+  public boolean hasTypeParameters() {
+      return mTypeParameters != null && !mTypeParameters.isEmpty();
+  }
+
   /**
    * Clone this MethodInfo as if it belonged to the specified ClassInfo and apply the
    * typeArgumentMapping to the parameters and return types.
@@ -668,7 +672,7 @@
   }
 
   public String typeArgumentsName(HashSet<String> typeVars) {
-    if (mTypeParameters == null || mTypeParameters.isEmpty()) {
+    if (!hasTypeParameters()) {
       return "";
     } else {
       return TypeInfo.typeArgumentsName(mTypeParameters, typeVars);
@@ -792,24 +796,37 @@
 
   public boolean isConsistent(MethodInfo mInfo) {
     boolean consistent = true;
-    if (this.mReturnType != mInfo.mReturnType && !this.mReturnType.equals(mInfo.mReturnType)) {
-      if (!mReturnType.isPrimitive() && !mInfo.mReturnType.isPrimitive()
-          && Objects.equals(mInfo.mReturnType.dimension(),  mReturnType.dimension())) {
-        // Check to see if our class extends the old class.
-        ApiInfo infoApi = mInfo.containingClass().containingPackage().containingApi();
-        ClassInfo infoReturnClass = infoApi.findClass(mInfo.mReturnType.qualifiedTypeName());
-        // Find the classes.
-        consistent = infoReturnClass != null &&
-                     infoReturnClass.isAssignableTo(mReturnType.qualifiedTypeName());
-      } else {
+    if (!mReturnType.isTypeVariable() && !mInfo.mReturnType.isTypeVariable()) {
+      if (!mReturnType.equals(mInfo.mReturnType) ||
+          mReturnType.dimension() != mInfo.mReturnType.dimension()) {
         consistent = false;
       }
-
-      if (!consistent) {
-        Errors.error(Errors.CHANGED_TYPE, mInfo.position(), "Method "
-            + mInfo.prettyQualifiedSignature() + " has changed return type from " + mReturnType
-            + " to " + mInfo.mReturnType);
+    } else if (!mReturnType.isTypeVariable() && mInfo.mReturnType.isTypeVariable()) {
+      List<ClassInfo> constraints = mInfo.resolveConstraints(mInfo.mReturnType);
+      for (ClassInfo constraint : constraints) {
+        if (!constraint.isAssignableTo(mReturnType.qualifiedTypeName())) {
+          consistent = false;
+        }
       }
+    } else if (mReturnType.isTypeVariable() && !mInfo.mReturnType.isTypeVariable()) {
+      // It's never valid to go from being a parameterized type to not being one.
+      // This would drop the implicit cast breaking backwards compatibility.
+      consistent = false;
+    } else {
+      // If both return types are parameterized then the constraints must be
+      // exactly the same.
+      List<ClassInfo> currentConstraints = mInfo.resolveConstraints(mReturnType);
+      List<ClassInfo> newConstraints = mInfo.resolveConstraints(mInfo.mReturnType);
+      if (currentConstraints.size() != newConstraints.size() ||
+          currentConstraints.retainAll(newConstraints)) {
+        consistent = false;
+      }
+    }
+
+    if (!consistent) {
+      Errors.error(Errors.CHANGED_TYPE, mInfo.position(), "Method "
+          + mInfo.prettyQualifiedSignature() + " has changed return type from " + mReturnType
+          + " to " + mInfo.mReturnType);
     }
 
     if (mIsAbstract != mInfo.mIsAbstract) {
@@ -898,6 +915,43 @@
     return consistent;
   }
 
+  private TypeInfo getTypeParameter(String qualifiedTypeName) {
+    if (hasTypeParameters()) {
+      for (TypeInfo parameter : mTypeParameters) {
+        if (parameter.qualifiedTypeName().equals(qualifiedTypeName)) {
+          return parameter;
+        }
+      }
+    }
+    return containingClass().getTypeParameter(qualifiedTypeName);
+  }
+
+  // Given a type parameter it returns a list of all of the classes and interfaces it must extend
+  // and implement.
+  private List<ClassInfo> resolveConstraints(TypeInfo type) {
+    ApiInfo api = containingClass().containingPackage().containingApi();
+    List<ClassInfo> classes = new ArrayList<>();
+    Queue<TypeInfo> types = new LinkedList<>();
+    types.add(type);
+    while (!types.isEmpty()) {
+      type = types.poll();
+      if (!type.isTypeVariable()) {
+        ClassInfo cl = api.findClass(type.qualifiedTypeName());
+        if (cl != null) {
+          classes.add(cl);
+        }
+      } else {
+        TypeInfo parameter = getTypeParameter(type.qualifiedTypeName());
+        if (parameter.extendsBounds() != null) {
+          for (TypeInfo bound : parameter.extendsBounds()) {
+            types.add(bound);
+          }
+        }
+      }
+    }
+    return classes;
+  }
+
   public void printResolutions() {
       if (mResolutions == null || mResolutions.isEmpty()) {
           return;
diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java
index dbec6f7..e1ada09 100644
--- a/src/com/google/doclava/Stubs.java
+++ b/src/com/google/doclava/Stubs.java
@@ -1343,6 +1343,10 @@
     apiWriter.print(cl.isInterface() ? "interface" : "class");
     apiWriter.print(" ");
     apiWriter.print(cl.name());
+    if (cl.hasTypeParameters()) {
+      apiWriter.print(TypeInfo.typeArgumentsName(cl.asTypeInfo().typeArguments(),
+          new HashSet<String>()));
+    }
 
     if (!cl.isInterface()
         && !"java.lang.Object".equals(cl.qualifiedName())
@@ -1432,6 +1436,9 @@
     if (mi.isSynchronized()) {
       apiWriter.print(" synchronized");
     }
+    if (mi.hasTypeParameters()) {
+      apiWriter.print(" " + mi.typeArgumentsName(new HashSet<String>()));
+    }
     apiWriter.print(" ");
     if (mi.returnType() == null) {
       apiWriter.print("void");
diff --git a/src/com/google/doclava/TypeInfo.java b/src/com/google/doclava/TypeInfo.java
index 567415b..ad26def 100644
--- a/src/com/google/doclava/TypeInfo.java
+++ b/src/com/google/doclava/TypeInfo.java
@@ -39,42 +39,75 @@
     if (typeString.endsWith("...")) {
       typeString = typeString.substring(0, typeString.length() - 3);
     }
-    
+
     // Generic parameters
+    int extendsPos = typeString.indexOf(" extends ");
     int paramStartPos = typeString.indexOf('<');
-    if (paramStartPos > -1) {
+    if (paramStartPos > -1 && (extendsPos == -1 || paramStartPos < extendsPos)) {
       ArrayList<TypeInfo> generics = new ArrayList<TypeInfo>();
-      int paramEndPos = typeString.lastIndexOf('>');
-      
+      int paramEndPos = 0;
+
       int entryStartPos = paramStartPos + 1;
       int bracketNesting = 0;
-      for (int i = entryStartPos; i < paramEndPos; i++) {
+      for (int i = entryStartPos; i < typeString.length(); i++) {
         char c = typeString.charAt(i);
         if (c == ',' && bracketNesting == 0) {
           String entry = typeString.substring(entryStartPos, i).trim();
           TypeInfo info = new TypeInfo(entry);
+          info.setIsTypeVariable(true);
           generics.add(info);
           entryStartPos = i + 1;
         } else if (c == '<') {
           bracketNesting++;
         } else if (c == '>') {
           bracketNesting--;
+          // Once bracketNesting goes negative, we've found the closing angle bracket
+          if (bracketNesting < 0) {
+            paramEndPos = i;
+            break;
+          }
+        }
+      }
+
+      TypeInfo info = new TypeInfo(typeString.substring(entryStartPos, paramEndPos).trim());
+      info.setIsTypeVariable(true);
+      generics.add(info);
+
+      mTypeArguments = generics;
+
+      if (paramEndPos < typeString.length() - 1) {
+        typeString = typeString.substring(0,paramStartPos) + typeString.substring(paramEndPos + 1);
+      } else {
+        typeString = typeString.substring(0,paramStartPos);
+      }
+    }
+
+    // The previous extends may have been within the generic type parameters which we don't
+    // actually care about and were removed from the type string above
+    extendsPos = typeString.indexOf(" extends ");
+    if (extendsPos > -1) {
+      ArrayList<TypeInfo> extendsBounds = new ArrayList<>();
+      int entryStartPos = extendsPos + 9;
+      int bracketNesting = 0;
+      for (int i = entryStartPos; i < typeString.length(); i++) {
+        char c = typeString.charAt(i);
+        if (c == '&' && bracketNesting == 0) {
+          String entry = typeString.substring(entryStartPos, i).trim();
+          TypeInfo info = new TypeInfo(entry);
+          extendsBounds.add(info);
+          entryStartPos = i + 1;
+        } else if (c == '<') {
+          bracketNesting++;
+        } else if (c == '>') {
+          bracketNesting--;
         }
       }
-     
-      TypeInfo info = new TypeInfo(typeString.substring(entryStartPos, paramEndPos).trim());
-      generics.add(info);
-      
-      mTypeArguments = generics;
-      
-      if (paramEndPos < typeString.length() - 1) {
-        typeString = typeString.substring(0,paramStartPos) + typeString.substring(paramEndPos + 1);
-      } else {
-        typeString = typeString.substring(0,paramStartPos);
-      }
+      TypeInfo info = new TypeInfo(typeString.substring(entryStartPos, typeString.length()).trim());
+      extendsBounds.add(info);
+      mExtendsBounds = extendsBounds;
+      typeString = typeString.substring(0, extendsPos);
     }
-    
-    // Dimensions
+
     int pos = typeString.indexOf('['); 
     if (pos > -1) {
       mDimension = typeString.substring(pos);
@@ -82,7 +115,7 @@
     } else {
       mDimension = "";
     }
-   
+
     if (PRIMITIVE_TYPES.contains(typeString)) {
       mIsPrimitive = true;
       mSimpleTypeName = typeString;
@@ -317,7 +350,7 @@
       mTypeArguments.add(arg);
   }
 
-  void setBounds(ArrayList<TypeInfo> superBounds, ArrayList<TypeInfo> extendsBounds) {
+  public void setBounds(ArrayList<TypeInfo> superBounds, ArrayList<TypeInfo> extendsBounds) {
     mSuperBounds = superBounds;
     mExtendsBounds = extendsBounds;
   }
@@ -330,7 +363,7 @@
       return mExtendsBounds;
   }
 
-  void setIsTypeVariable(boolean b) {
+  public void setIsTypeVariable(boolean b) {
     mIsTypeVariable = b;
   }
 
@@ -342,7 +375,7 @@
       return mIsWildcard;
   }
 
-  static HashSet<String> typeVariables(ArrayList<TypeInfo> params) {
+  public static HashSet<String> typeVariables(ArrayList<TypeInfo> params) {
     return typeVariables(params, new HashSet<String>());
   }
 
@@ -362,6 +395,16 @@
     return mIsTypeVariable;
   }
 
+  public void resolveTypeVariables(HashSet<String> variables) {
+    if (mExtendsBounds != null) {
+      for (TypeInfo bound : mExtendsBounds) {
+        if (variables.contains(bound.qualifiedTypeName())) {
+          bound.setIsTypeVariable(true);
+        }
+      }
+    }
+  }
+
   public String defaultValue() {
     if (mIsPrimitive) {
       if ("boolean".equals(mSimpleTypeName)) {
@@ -477,7 +520,7 @@
     Map<String, TypeInfo> map = new HashMap<String, TypeInfo>();
     for (int i = 0; i < generic.typeArguments().size(); i++) {
       if (typed.typeArguments() != null && typed.typeArguments().size() > i) {
-        map.put(generic.typeArguments().get(i).fullName(), typed.typeArguments().get(i));
+        map.put(generic.typeArguments().get(i).simpleTypeName(), typed.typeArguments().get(i));
       }
     }
     return map;
diff --git a/src/com/google/doclava/apicheck/ApiFile.java b/src/com/google/doclava/apicheck/ApiFile.java
index 2d958d4..d865e20 100644
--- a/src/com/google/doclava/apicheck/ApiFile.java
+++ b/src/com/google/doclava/apicheck/ApiFile.java
@@ -30,6 +30,7 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -163,12 +164,15 @@
     }
     assertIdent(tokenizer, token);
     name = token;
-    token = tokenizer.requireToken();
     qname = qualifiedName(pkg.name(), name, null);
+    final TypeInfo typeInfo = Converter.obtainTypeFromString(qname);
+    token = tokenizer.requireToken();
     cl = new ClassInfo(null/*classDoc*/, ""/*rawCommentText*/, tokenizer.pos(), pub, prot, 
         pkgpriv, false/*isPrivate*/, stat, iface, abs, true/*isOrdinaryClass*/, 
         false/*isException*/, false/*isError*/, false/*isEnum*/, false/*isAnnotation*/,
-        fin, false/*isIncluded*/, name, qname, null/*qualifiedTypeName*/, false/*isPrimitive*/);
+        fin, false/*isIncluded*/, typeInfo.simpleTypeName(), typeInfo.qualifiedTypeName(),
+        null/*qualifiedTypeName*/, false/*isPrimitive*/);
+    cl.setTypeInfo(typeInfo);
     cl.setDeprecated(dep);
     if ("extends".equals(token)) {
       token = tokenizer.requireToken();
@@ -178,8 +182,6 @@
     }
     // Resolve superclass after done parsing
     api.mapClassToSuper(cl, ext);
-    final TypeInfo typeInfo = Converter.obtainTypeFromString(qname) ;
-    cl.setTypeInfo(typeInfo);
     cl.setAnnotations(new ArrayList<AnnotationInstanceInfo>());
     if ("implements".equals(token)) {
       while (true) {
@@ -261,7 +263,7 @@
         new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
     method.setDeprecated(dep);
     token = tokenizer.requireToken();
-    parseParameterList(tokenizer, method, token);
+    parseParameterList(tokenizer, method, new HashSet<String>(), token);
     token = tokenizer.requireToken();
     if ("throws".equals(token)) {
       token = parseThrows(tokenizer, method);
@@ -283,7 +285,9 @@
     boolean dep = false;
     boolean syn = false;
     boolean def = false;
-    String type;
+    ArrayList<TypeInfo> typeParameters = new ArrayList<>();
+    TypeInfo returnType;
+    HashSet<String> typeVariableNames;
     String name;
     String ext = null;
     MethodInfo method;
@@ -321,25 +325,32 @@
       syn = true;
       token = tokenizer.requireToken();
     }
+    if ("<".equals(token)) {
+      parseTypeParameterList(tokenizer, typeParameters, cl);
+      token = tokenizer.requireToken();
+    }
     assertIdent(tokenizer, token);
-    type = token;
+    returnType = Converter.obtainTypeFromString(token);
+    typeVariableNames = TypeInfo.typeVariables(typeParameters);
+    if (typeVariableNames.contains(returnType.qualifiedTypeName())) {
+      returnType.setIsTypeVariable(true);
+    }
     token = tokenizer.requireToken();
     assertIdent(tokenizer, token);
     name = token;
-    method = new MethodInfo(""/*rawCommentText*/, new ArrayList<TypeInfo>()/*typeParameters*/,
-        name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin,
-        stat, false/*isSynthetic*/, abs/*isAbstract*/, syn, false/*isNative*/, def/*isDefault*/,
-        false /*isAnnotationElement*/, "method", null/*flatSignature*/, null/*overriddenMethod*/,
-        Converter.obtainTypeFromString(type), new ArrayList<ParameterInfo>(),
-        new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
-        new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
+    method = new MethodInfo(""/*rawCommentText*/, typeParameters, name, null/*signature*/, cl, cl,
+        pub, prot, pkgpriv, false/*isPrivate*/, fin, stat, false/*isSynthetic*/, abs/*isAbstract*/,
+        syn, false/*isNative*/, def/*isDefault*/, false /*isAnnotationElement*/, "method",
+        null/*flatSignature*/, null/*overriddenMethod*/, returnType,
+        new ArrayList<ParameterInfo>(), new ArrayList<ClassInfo>()/*thrownExceptions*/,
+        tokenizer.pos(), new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
     method.setDeprecated(dep);
     token = tokenizer.requireToken();
     if (!"(".equals(token)) {
       throw new ApiParseException("expected (", tokenizer.getLine());
     }
     token = tokenizer.requireToken();
-    parseParameterList(tokenizer, method, token);
+    parseParameterList(tokenizer, method, typeVariableNames, token);
     token = tokenizer.requireToken();
     if ("throws".equals(token)) {
       token = parseThrows(tokenizer, method);
@@ -475,8 +486,47 @@
     }
   }
 
+  private static void parseTypeParameterList(Tokenizer tokenizer,
+      List<TypeInfo> methodTypeParameters, ClassInfo cl) throws ApiParseException {
+    String token;
+    HashSet<String> variables = cl.typeVariables();
+    do {
+      token = tokenizer.requireToken();
+      assertIdent(tokenizer, token);
+      TypeInfo type = new TypeInfo(token);
+      type.setIsTypeVariable(true);
+      variables.add(type.qualifiedTypeName());
+      ArrayList<TypeInfo> extendsBounds = new ArrayList<>();
+      token = tokenizer.requireToken();
+      if ("extends".equals(token)) {
+        do {
+          token = tokenizer.requireToken();
+          assertIdent(tokenizer, token);
+          extendsBounds.add(new TypeInfo(token));
+          token = tokenizer.requireToken();
+        } while ("&".equals(token));
+      }
+      if (!extendsBounds.isEmpty()) {
+        type.setBounds(null, extendsBounds);
+      }
+      methodTypeParameters.add(type);
+    } while (",".equals(token));
+
+    // Type variables aren't guaranteed to be declared before they're referenced so we need to wait
+    // until after we've processed them all to figure out which ones are type variables and which
+    // ones are classes (which we may not have processed yet either).
+    for (TypeInfo type : methodTypeParameters) {
+      type.resolveTypeVariables(variables);
+    }
+
+    if (!">".equals(token)) {
+      throw new ApiParseException("Expected '>' to end type parameter list, found "
+          + token, tokenizer.getLine());
+    }
+  }
+
   private static void parseParameterList(Tokenizer tokenizer, AbstractMethodInfo method,
-      String token) throws ApiParseException {
+      HashSet<String> typeParameters, String token) throws ApiParseException {
     while (true) {
       if (")".equals(token)) {
         return;
@@ -497,8 +547,12 @@
       }
       // api file does not preserve annotations.
       List<AnnotationInstanceInfo> annotations = Collections.emptyList();
+      TypeInfo typeInfo = Converter.obtainTypeFromString(type);
+      if (typeParameters.contains(typeInfo.qualifiedTypeName())) {
+        typeInfo.setIsTypeVariable(true);
+      }
       method.addParameter(new ParameterInfo(name, type,
-            Converter.obtainTypeFromString(type),
+            typeInfo,
             type.endsWith("..."),
             tokenizer.pos(),
             annotations));
@@ -660,10 +714,10 @@
             if (mBuf[mPos] == '<') {
               genericDepth++;
               mPos++;
-            } else if (mBuf[mPos] == '>') {
-              genericDepth--;
-              mPos++;
             } else if (genericDepth != 0) {
+              if (mBuf[mPos] == '>') {
+                genericDepth--;
+              }
               mPos++;
             }
           }