content: package com.alibaba.android.arouter.compiler.processor;
  
  import com.alibaba.android.arouter.compiler.entity.RouteDoc;
  import com.alibaba.android.arouter.compiler.utils.Consts;
  import com.alibaba.android.arouter.facade.annotation.Autowired;
  import com.alibaba.android.arouter.facade.annotation.Route;
  import com.alibaba.android.arouter.facade.enums.RouteType;
  import com.alibaba.android.arouter.facade.enums.TypeKind;
  import com.alibaba.android.arouter.facade.model.RouteMeta;
  import com.alibaba.fastjson.JSON;
  import com.alibaba.fastjson.serializer.SerializerFeature;
  import com.google.auto.service.AutoService;
  import com.squareup.javapoet.ClassName;
  import com.squareup.javapoet.JavaFile;
  import com.squareup.javapoet.MethodSpec;
  import com.squareup.javapoet.ParameterSpec;
  import com.squareup.javapoet.ParameterizedTypeName;
  import com.squareup.javapoet.TypeSpec;
  import com.squareup.javapoet.WildcardTypeName;
  
  import org.apache.commons.collections4.CollectionUtils;
  import org.apache.commons.collections4.MapUtils;
  import org.apache.commons.lang3.StringUtils;
  
  import java.io.IOException;
  import java.io.Writer;
  import java.util.ArrayList;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
  import java.util.TreeSet;
  
  import javax.annotation.processing.ProcessingEnvironment;
  import javax.annotation.processing.Processor;
  import javax.annotation.processing.RoundEnvironment;
  import javax.annotation.processing.SupportedAnnotationTypes;
  import javax.lang.model.element.Element;
  import javax.lang.model.element.TypeElement;
  import javax.lang.model.type.DeclaredType;
  import javax.lang.model.type.TypeMirror;
  import javax.tools.StandardLocation;
  
  import static com.alibaba.android.arouter.compiler.utils.Consts.ACTIVITY;
  import static com.alibaba.android.arouter.compiler.utils.Consts.ANNOTATION_TYPE_AUTOWIRED;
  import static com.alibaba.android.arouter.compiler.utils.Consts.ANNOTATION_TYPE_ROUTE;
  import static com.alibaba.android.arouter.compiler.utils.Consts.FRAGMENT;
  import static com.alibaba.android.arouter.compiler.utils.Consts.IPROVIDER_GROUP;
  import static com.alibaba.android.arouter.compiler.utils.Consts.IROUTE_GROUP;
  import static com.alibaba.android.arouter.compiler.utils.Consts.ITROUTE_ROOT;
  import static com.alibaba.android.arouter.compiler.utils.Consts.METHOD_LOAD_INTO;
  import static com.alibaba.android.arouter.compiler.utils.Consts.NAME_OF_GROUP;
  import static com.alibaba.android.arouter.compiler.utils.Consts.NAME_OF_PROVIDER;
  import static com.alibaba.android.arouter.compiler.utils.Consts.NAME_OF_ROOT;
  import static com.alibaba.android.arouter.compiler.utils.Consts.PACKAGE_OF_GENERATE_DOCS;
  import static com.alibaba.android.arouter.compiler.utils.Consts.PACKAGE_OF_GENERATE_FILE;
  import static com.alibaba.android.arouter.compiler.utils.Consts.SEPARATOR;
  import static com.alibaba.android.arouter.compiler.utils.Consts.SERVICE;
  import static com.alibaba.android.arouter.compiler.utils.Consts.WARNING_TIPS;
  import static javax.lang.model.element.Modifier.PUBLIC;
  
  /**
   * A processor used for find route.
   *
   * @author Alex <a href="mailto:zhilong.liu@aliyun.com">Contact me.</a>
   * @version 1.0
   * @since 16/8/15 下午10:08
   */
  @AutoService(Processor.class)
  @SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED})
  public class RouteProcessor extends BaseProcessor {
      private Map<String, Set<RouteMeta>> groupMap = new HashMap<>(); // ModuleName and routeMeta.
      private Map<String, String> rootMap = new TreeMap<>();  // Map of root metas, used for generate class file in order.
  
      private TypeMirror iProvider = null;
      private Writer docWriter;       // Writer used for write doc
  
      @Override
      public synchronized void init(ProcessingEnvironment processingEnv) {
          super.init(processingEnv);
  
          if (generateDoc) {
              try {
                  docWriter = mFiler.createResource(
                          StandardLocation.SOURCE_OUTPUT,
                          PACKAGE_OF_GENERATE_DOCS,
                          "arouter-map-of-" + moduleName + ".json"
                  ).openWriter();
              } catch (IOException e) {
                  logger.error("Create doc writer failed, because " + e.getMessage());
              }
          }
  
          iProvider = elementUtils.getTypeElement(Consts.IPROVIDER).asType();
  
          logger.info(">>> RouteProcessor init. <<<");
      }
  
      /**
       * {@inheritDoc}
       *
       * @param annotations
       * @param roundEnv
       */
      @Override
      public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
          if (CollectionUtils.isNotEmpty(annotations)) {
              Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
              try {
                  logger.info(">>> Found routes, start... <<<");
                  this.parseRoutes(routeElements);
  
              } catch (Exception e) {
                  logger.error(e);
              }
              return true;
          }
  
          return false;
      }
  
      private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
          if (CollectionUtils.isNotEmpty(routeElements)) {
              // prepare the type an so on.
  
              logger.info(">>> Found routes, size is " + routeElements.size() + " <<<");
  
              rootMap.clear();
  
              TypeMirror type_Activity = elementUtils.getTypeElement(ACTIVITY).asType();
              TypeMirror type_Service = elementUtils.getTypeElement(SERVICE).asType();
              TypeMirror fragmentTm = elementUtils.getTypeElement(FRAGMENT).asType();
              TypeMirror fragmentTmV4 = elementUtils.getTypeElement(Consts.FRAGMENT_V4).asType();
  
              // Interface of ARouter
              TypeElement type_IRouteGroup = elementUtils.getTypeElement(IROUTE_GROUP);
              TypeElement type_IProviderGroup = elementUtils.getTypeElement(IPROVIDER_GROUP);
              ClassName routeMetaCn = ClassName.get(RouteMeta.class);
              ClassName routeTypeCn = ClassName.get(RouteType.class);
  
              /*
                 Build input type, format as :
  
                 ```Map<String, Class<? extends IRouteGroup>>```
               */
              ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(
                      ClassName.get(Map.class),
                      ClassName.get(String.class),
                      ParameterizedTypeName.get(
                              ClassName.get(Class.class),
                              WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup))
                      )
              );
  
              /*
  
                ```Map<String, RouteMeta>```
               */
              ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
                      ClassName.get(Map.class),
                      ClassName.get(String.class),
                      ClassName.get(RouteMeta.class)
              );
  
              /*
                Build input param name.
               */
              ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build();
              ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
              ParameterSpec providerParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "providers").build();  // Ps. its param type same as groupParamSpec!
  
              /*
                Build method : 'loadInto'
               */
              MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                      .addAnnotation(Override.class)
                      .addModifiers(PUBLIC)
                      .addParameter(rootParamSpec);
  
              //  Follow a sequence, find out metas of group first, generate java file, then statistics them as root.
              for (Element element : routeElements) {
                  TypeMirror tm = element.asType();
                  Route route = element.getAnnotation(Route.class);
                  RouteMeta routeMeta;
  
                  // Activity or Fragment
                  if (types.isSubtype(tm, type_Activity) || types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {
                      // Get all fields annotation by @Autowired
                      Map<String, Integer> paramsType = new HashMap<>();
                      Map<String, Autowired> injectConfig = new HashMap<>();
                      injectParamCollector(element, paramsType, injectConfig);
  
                      if (types.isSubtype(tm, type_Activity)) {
                          // Activity
                          logger.info(">>> Found activity route: " + tm.toString() + " <<<");
                          routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
                      } else {
                          // Fragment
                          logger.info(">>> Found fragment route: " + tm.toString() + " <<<");
                          routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), paramsType);
                      }
  
                      routeMeta.setInjectConfig(injectConfig);
                  } else if (types.isSubtype(tm, iProvider)) {         // IProvider
                      logger.info(">>> Found provider route: " + tm.toString() + " <<<");
                      routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null);
                  } else if (types.isSubtype(tm, type_Service)) {           // Service
                      logger.info(">>> Found service route: " + tm.toString() + " <<<");
                      routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
                  } else {
                      throw new RuntimeException("The @Route is marked on unsupported class, look at [" + tm.toString() + "].");
                  }
  
                  categories(routeMeta);
              }
  
              MethodSpec.Builder loadIntoMethodOfProviderBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                      .addAnnotation(Override.class)
                      .addModifiers(PUBLIC)
                      .addParameter(providerParamSpec);
  
              Map<String, List<RouteDoc>> docSource = new HashMap<>();
  
              // Start generate java source, structure is divided into upper and lower levels, used for demand initialization.
              for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
                  String groupName = entry.getKey();
  
                  MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                          .addAnnotation(Override.class)
                          .addModifiers(PUBLIC)
                          .addParameter(groupParamSpec);
  
                  List<RouteDoc> routeDocList = new ArrayList<>();
  
                  // Build group method body
                  Set<RouteMeta> groupData = entry.getValue();
                  for (RouteMeta routeMeta : groupData) {
                      RouteDoc routeDoc = extractDocInfo(routeMeta);
  
                      ClassName className = ClassName.get((TypeElement) routeMeta.getRawType());
  
                      switch (routeMeta.getType()) {
                          case PROVIDER:  // Need cache provider's super class
                              List<? extends TypeMirror> interfaces = ((TypeElement) routeMeta.getRawType()).getInterfaces();
                              for (TypeMirror tm : interfaces) {
                                  routeDoc.addPrototype(tm.toString());
  
                                  if (types.isSameType(tm, iProvider)) {   // Its implements iProvider interface himself.
                                      // This interface extend the IProvider, so it can be used for mark provider
                                      loadIntoMethodOfProviderBuilder.addStatement(
                                              "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                                              (routeMeta.getRawType()).toString(),
                                              routeMetaCn,
                                              routeTypeCn,
                                              className,
                                              routeMeta.getPath(),
                                              routeMeta.getGroup());
                                  } else if (types.isSubtype(tm, iProvider)) {
                                      // This interface extend the IProvider, so it can be used for mark provider
                                      loadIntoMethodOfProviderBuilder.addStatement(
                                              "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                                              tm.toString(),    // So stupid, will duplicate only save class name.
                                              routeMetaCn,
                                              routeTypeCn,
                                              className,
                                              routeMeta.getPath(),
                                              routeMeta.getGroup());
                                  }
                              }
                              break;
                          default:
                              break;
                      }
  
                      // Make map body for paramsType
                      StringBuilder mapBodyBuilder = new StringBuilder();
                      Map<String, Integer> paramsType = routeMeta.getParamsType();
                      Map<String, Autowired> injectConfigs = routeMeta.getInjectConfig();
                      if (MapUtils.isNotEmpty(paramsType)) {
                          List<RouteDoc.Param> paramList = new ArrayList<>();
  
                          for (Map.Entry<String, Integer> types : paramsType.entrySet()) {
                              mapBodyBuilder.append("put(\"").append(types.getKey()).append("\", ").append(types.getValue()).append("); ");
  
                              RouteDoc.Param param = new RouteDoc.Param();
                              Autowired injectConfig = injectConfigs.get(types.getKey());
                              param.setKey(types.getKey());
                              param.setType(TypeKind.values()[types.getValue()].name().toLowerCase());
                              param.setDescription(injectConfig.desc());
                              param.setRequired(injectConfig.required());
  
                              paramList.add(param);
                          }
  
                          routeDoc.setParams(paramList);
                      }
                      String mapBody = mapBodyBuilder.toString();
  
                      loadIntoMethodOfGroupBuilder.addStatement(
                              "atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                              routeMeta.getPath(),
                              routeMetaCn,
                              routeTypeCn,
                              className,
                              routeMeta.getPath().toLowerCase(),
                              routeMeta.getGroup().toLowerCase());
  
                      routeDoc.setClassName(className.toString());
                      routeDocList.add(routeDoc);
                  }
  
                  // Generate groups
                  String groupFileName = NAME_OF_GROUP + groupName;
                  JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                          TypeSpec.classBuilder(groupFileName)
                                  .addJavadoc(WARNING_TIPS)
                                  .addSuperinterface(ClassName.get(type_IRouteGroup))
                                  .addModifiers(PUBLIC)
                                  .addMethod(loadIntoMethodOfGroupBuilder.build())
                                  .build()
                  ).build().writeTo(mFiler);
  
                  logger.info(">>> Generated group: " + groupName + "<<<");
                  rootMap.put(groupName, groupFileName);
                  docSource.put(groupName, routeDocList);
              }
  
              if (MapUtils.isNotEmpty(rootMap)) {
                  // Generate root meta by group name, it must be generated before root, then I can find out the class of group.
                  for (Map.Entry<String, String> entry : rootMap.entrySet()) {
                      loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
                  }
              }
  
              // Output route doc
              if (generateDoc) {
                  docWriter.append(JSON.toJSONString(docSource, SerializerFeature.PrettyFormat));
                  docWriter.flush();
                  docWriter.close();
              }
  
              // Write provider into disk
              String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;
              JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                      TypeSpec.classBuilder(providerMapFileName)
                              .addJavadoc(WARNING_TIPS)
                              .addSuperinterface(ClassName.get(type_IProviderGroup))
                              .addModifiers(PUBLIC)
                              .addMethod(loadIntoMethodOfProviderBuilder.build())
                              .build()
              ).build().writeTo(mFiler);
  
              logger.info(">>> Generated provider map, name is " + providerMapFileName + " <<<");
  
              // Write root meta into disk.
              String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
              JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                      TypeSpec.classBuilder(rootFileName)
                              .addJavadoc(WARNING_TIPS)
                              .addSuperinterface(ClassName.get(elementUtils.getTypeElement(ITROUTE_ROOT)))
                              .addModifiers(PUBLIC)
                              .addMethod(loadIntoMethodOfRootBuilder.build())
                              .build()
              ).build().writeTo(mFiler);
  
              logger.info(">>> Generated root, name is " + rootFileName + " <<<");
          }
      }
  
      /**
       * Recursive inject config collector.
       *
       * @param element current element.
       */
      private void injectParamCollector(Element element, Map<String, Integer> paramsType, Map<String, Autowired> injectConfig) {
          for (Element field : element.getEnclosedElements()) {
              if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !types.isSubtype(field.asType(), iProvider)) {
                  // It must be field, then it has annotation, but it not be provider.
                  Autowired paramConfig = field.getAnnotation(Autowired.class);
                  String injectName = StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name();
                  paramsType.put(injectName, typeUtils.typeExchange(field));
                  injectConfig.put(injectName, paramConfig);
              }
          }
  
          // if has parent?
          TypeMirror parent = ((TypeElement) element).getSuperclass();
          if (parent instanceof DeclaredType) {
              Element parentElement = ((DeclaredType) parent).asElement();
              if (parentElement instanceof TypeElement && !((TypeElement) parentElement).getQualifiedName().toString().startsWith("android")) {
                  injectParamCollector(parentElement, paramsType, injectConfig);
              }
          }
      }
  
      /**
       * Extra doc info from route meta
       *
       * @param routeMeta meta
       * @return doc
       */
      private RouteDoc extractDocInfo(RouteMeta routeMeta) {
          RouteDoc routeDoc = new RouteDoc();
          routeDoc.setGroup(routeMeta.getGroup());
          routeDoc.setPath(routeMeta.getPath());
          routeDoc.setDescription(routeMeta.getName());
          routeDoc.setType(routeMeta.getType().name().toLowerCase());
          routeDoc.setMark(routeMeta.getExtra());
  
          return routeDoc;
      }
  
      /**
       * Sort metas in group.
       *
       * @param routeMete metas.
       */
      private void categories(RouteMeta routeMete) {
          if (routeVerify(routeMete)) {
              logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<");
              Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
              if (CollectionUtils.isEmpty(routeMetas)) {
                  Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
                      @Override
                      public int compare(RouteMeta r1, RouteMeta r2) {
                          try {
                              return r1.getPath().compareTo(r2.getPath());
                          } catch (NullPointerException npe) {
                              logger.error(npe.getMessage());
                              return 0;
                          }
                      }
                  });
                  routeMetaSet.add(routeMete);
                  groupMap.put(routeMete.getGroup(), routeMetaSet);
              } else {
                  routeMetas.add(routeMete);
              }
          } else {
              logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<");
          }
      }
  
      /**
       * Verify the route meta
       *
       * @param meta raw meta
       */
      private boolean routeVerify(RouteMeta meta) {
          String path = meta.getPath();
  
          if (StringUtils.isEmpty(path) || !path.startsWith("/")) {   // The path must be start with '/' and not empty!
              return false;
          }
  
          if (StringUtils.isEmpty(meta.getGroup())) { // Use default group(the first word in path)
              try {
                  String defaultGroup = path.substring(1, path.indexOf("/", 1));
                  if (StringUtils.isEmpty(defaultGroup)) {
                      return false;
                  }
  
                  meta.setGroup(defaultGroup);
                  return true;
              } catch (Exception e) {
                  logger.error("Failed to extract default group! " + e.getMessage());
                  return false;
              }
          }
  
          return true;
      }
  }
  