001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.tools.plugin.javadoc;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Objects;
024import java.util.Optional;
025import java.util.StringTokenizer;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029/**
030 * Describes a code reference used in javadoc tags {@code see}, {@code link} and {@code linkplain}.
031 * The format of the reference given as string is {@code module/package.class#member label}.
032 * Members must be separated with a {@code #} to be detected.
033 * Targets either module, package, class or field/method/constructor in class.
034 * This class does not know whether the second part part refers to a package, class or both,
035 * as they use the same alphabet and separators.
036 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html#link">link tag specification</a>
037 */
038public class JavadocReference {
039    private final Optional<String> moduleName;
040
041    private final Optional<String> packageNameClassName;
042
043    private final Optional<String> member; // optional, but may appear with both className and packageName being null
044
045    private final Optional<String> label;
046
047    /*
048     * Test at https://regex101.com/r/eDzWNx
049     * Captures several groups: module name (1), package name and/or class name (2), member (3), label (4)
050     */
051    private static final Pattern REFERENCE_VALUE_PATTERN =
052            Pattern.compile("^\\s*(?:(.+)/)??([^#\\s/]+)?(?:#([^\\s\\(]+(?:\\([^\\)]*\\))?))?(?: +(.*))?$");
053
054    private static final int GROUP_INDEX_MODULE = 1;
055
056    private static final int GROUP_INDEX_PACKAGECLASS = 2;
057
058    private static final int GROUP_INDEX_MEMBER = 3;
059
060    private static final int GROUP_INDEX_LABEL = 4;
061
062    /**
063     *
064     * @param reference the reference value to parse
065     * @return the created {@link JavadocReference}
066     * @throws IllegalArgumentException in case the reference has an invalid format
067     */
068    public static JavadocReference parse(String reference) {
069        // must match the behaviour of com.sun.tools.javac.parser.ReferenceParser#parseReference
070        Matcher matcher = REFERENCE_VALUE_PATTERN.matcher(reference);
071        if (!matcher.matches()) {
072            throw new IllegalArgumentException("Invalid format of javadoc reference: " + reference);
073        }
074        final Optional<String> moduleName = getOptionalGroup(matcher, GROUP_INDEX_MODULE);
075        final Optional<String> packageNameClassName = getOptionalGroup(matcher, GROUP_INDEX_PACKAGECLASS);
076        final Optional<String> member =
077                getOptionalGroup(matcher, GROUP_INDEX_MEMBER).map(JavadocReference::normalizeMember);
078        final Optional<String> label = getOptionalGroup(matcher, GROUP_INDEX_LABEL);
079        return new JavadocReference(moduleName, packageNameClassName, member, label);
080    }
081
082    private static Optional<String> getOptionalGroup(Matcher matcher, int index) {
083        String group = matcher.group(index);
084        if (group != null && !group.isEmpty()) {
085            return Optional.of(group);
086        } else {
087            return Optional.empty();
088        }
089    }
090
091    JavadocReference(
092            Optional<String> moduleName,
093            Optional<String> packageNameClassName,
094            Optional<String> member,
095            Optional<String> label) {
096        this.moduleName = moduleName;
097        this.packageNameClassName = packageNameClassName;
098        this.member = member;
099        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}