View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.tools.plugin.javadoc;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Objects;
24  import java.util.Optional;
25  import java.util.StringTokenizer;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  /**
30   * Describes a code reference used in javadoc tags {@code see}, {@code link} and {@code linkplain}.
31   * The format of the reference given as string is {@code module/package.class#member label}.
32   * Members must be separated with a {@code #} to be detected.
33   * Targets either module, package, class or field/method/constructor in class.
34   * This class does not know whether the second part part refers to a package, class or both,
35   * as they use the same alphabet and separators.
36   * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html#link">link tag specification</a>
37   */
38  public class JavadocReference {
39      private final Optional<String> moduleName;
40  
41      private final Optional<String> packageNameClassName;
42  
43      private final Optional<String> member; // optional, but may appear with both className and packageName being null
44  
45      private final Optional<String> label;
46  
47      /*
48       * Test at https://regex101.com/r/eDzWNx
49       * Captures several groups: module name (1), package name and/or class name (2), member (3), label (4)
50       */
51      private static final Pattern REFERENCE_VALUE_PATTERN =
52              Pattern.compile("^\\s*(?:(.+)/)??([^#\\s/]+)?(?:#([^\\s\\(]+(?:\\([^\\)]*\\))?))?(?: +(.*))?$");
53  
54      private static final int GROUP_INDEX_MODULE = 1;
55  
56      private static final int GROUP_INDEX_PACKAGECLASS = 2;
57  
58      private static final int GROUP_INDEX_MEMBER = 3;
59  
60      private static final int GROUP_INDEX_LABEL = 4;
61  
62      /**
63       *
64       * @param reference the reference value to parse
65       * @return the created {@link JavadocReference}
66       * @throws IllegalArgumentException in case the reference has an invalid format
67       */
68      public static JavadocReference parse(String reference) {
69          // must match the behaviour of com.sun.tools.javac.parser.ReferenceParser#parseReference
70          Matcher matcher = REFERENCE_VALUE_PATTERN.matcher(reference);
71          if (!matcher.matches()) {
72              throw new IllegalArgumentException("Invalid format of javadoc reference: " + reference);
73          }
74          final Optional<String> moduleName = getOptionalGroup(matcher, GROUP_INDEX_MODULE);
75          final Optional<String> packageNameClassName = getOptionalGroup(matcher, GROUP_INDEX_PACKAGECLASS);
76          final Optional<String> member =
77                  getOptionalGroup(matcher, GROUP_INDEX_MEMBER).map(JavadocReference::normalizeMember);
78          final Optional<String> label = getOptionalGroup(matcher, GROUP_INDEX_LABEL);
79          return new JavadocReference(moduleName, packageNameClassName, member, label);
80      }
81  
82      private static Optional<String> getOptionalGroup(Matcher matcher, int index) {
83          String group = matcher.group(index);
84          if (group != null && !group.isEmpty()) {
85              return Optional.of(group);
86          } else {
87              return Optional.empty();
88          }
89      }
90  
91      JavadocReference(
92              Optional<String> moduleName,
93              Optional<String> packageNameClassName,
94              Optional<String> member,
95              Optional<String> label) {
96          this.moduleName = moduleName;
97          this.packageNameClassName = packageNameClassName;
98          this.member = member;
99          this.label = label;
100     }
101 
102     public Optional<String> getModuleName() {
103         return moduleName;
104     }
105 
106     /**
107      *
108      * @return a package name, a class name or a package name followed by a class name
109      */
110     public Optional<String> getPackageNameClassName() {
111         return packageNameClassName;
112     }
113 
114     public Optional<String> getMember() {
115         return member;
116     }
117 
118     public Optional<String> getLabel() {
119         return label;
120     }
121 
122     @Override
123     public String toString() {
124         return "JavadocReference [moduleName=" + moduleName + ", packageNameClassName=" + packageNameClassName
125                 + ", member=" + member + ", label=" + label + "]";
126     }
127 
128     @Override
129     public int hashCode() {
130         return Objects.hash(label, member, packageNameClassName, moduleName);
131     }
132 
133     @Override
134     public boolean equals(Object obj) {
135         if (this == obj) {
136             return true;
137         }
138         if (obj == null) {
139             return false;
140         }
141         if (getClass() != obj.getClass()) {
142             return false;
143         }
144         JavadocReference other = (JavadocReference) obj;
145         return Objects.equals(label, other.label)
146                 && Objects.equals(member, other.member)
147                 && Objects.equals(packageNameClassName, other.packageNameClassName)
148                 && Objects.equals(moduleName, other.moduleName);
149     }
150 
151     static String normalizeMember(String member) {
152         // for methods:
153         int indexOfParenthesis = member.indexOf('(');
154         if (indexOfParenthesis != -1) {
155             if (!member.endsWith(")")) {
156                 throw new IllegalArgumentException("Invalid format of javadoc reference member: " + member);
157             }
158             // for each parameter separated by a comma, remove the parameter name and keep only the type
159             StringTokenizer tokenizer =
160                     new StringTokenizer(member.substring(indexOfParenthesis + 1, member.length() - 1), ",");
161             Collection<String> parameterTypes = new ArrayList<>();
162             while (tokenizer.hasMoreTokens()) {
163                 String parameter = tokenizer.nextToken().trim();
164                 // remove parameter name if present
165                 if (parameter.contains(" ")) {
166                     parameterTypes.add(parameter.substring(0, parameter.indexOf(' ')));
167                 } else {
168                     parameterTypes.add(parameter);
169                 }
170             }
171             member = member.substring(0, indexOfParenthesis) + "(" + String.join(",", parameterTypes) + ")";
172         }
173         // remove whitespace
174         member = member.replaceAll("\\s+", "");
175         return member;
176     }
177 }