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.Objects;
022import java.util.Optional;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import org.codehaus.plexus.util.StringUtils;
027
028/**
029 * Describes a code reference used in javadoc tags {@code see}, {@code link} and {@code linkplain}.
030 * The format of the reference given as string is {@code module/package.class#member label}.
031 * Members must be separated with a {@code #} to be detected.
032 * Targets either module, package, class or field/method/constructor in class.
033 * This class does not know whether the second part part refers to a package, class or both,
034 * as they use the same alphabet and separators.
035 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/javadoc/doc-comment-spec.html#link">link tag specification</a>
036 */
037public class JavadocReference {
038    private final Optional<String> moduleName;
039
040    private final Optional<String> packageNameClassName;
041
042    private final Optional<String> member; // optional, but may appear with both className and packageName being null
043
044    private final Optional<String> label;
045
046    /*
047     * Test at https://regex101.com/r/eDzWNx
048     * Captures several groups: module name (1), package name and/or class name (2), member (3), label (4)
049     */
050    private static final Pattern REFERENCE_VALUE_PATTERN =
051            Pattern.compile("^\\s*(?:(.+)/)??([^#\\s/]+)?(?:#([^\\s\\(]+(?:\\([^\\)]*\\))?))?(?: +(.*))?$");
052
053    private static final int GROUP_INDEX_MODULE = 1;
054
055    private static final int GROUP_INDEX_PACKAGECLASS = 2;
056
057    private static final int GROUP_INDEX_MEMBER = 3;
058
059    private static final int GROUP_INDEX_LABEL = 4;
060
061    /**
062     *
063     * @param reference the reference value to parse
064     * @return the created {@link JavadocReference}
065     * @throws IllegalArgumentException in case the reference has an invalid format
066     */
067    public static JavadocReference parse(String reference) {
068        Matcher matcher = REFERENCE_VALUE_PATTERN.matcher(reference);
069        if (!matcher.matches()) {
070            throw new IllegalArgumentException("Invalid format of javadoc reference: " + reference);
071        }
072        final Optional<String> moduleName = getOptionalGroup(matcher, GROUP_INDEX_MODULE);
073        final Optional<String> packageNameClassName = getOptionalGroup(matcher, GROUP_INDEX_PACKAGECLASS);
074        final Optional<String> member = getOptionalGroup(matcher, GROUP_INDEX_MEMBER);
075        final Optional<String> label = getOptionalGroup(matcher, GROUP_INDEX_LABEL);
076        return new JavadocReference(moduleName, packageNameClassName, member, label);
077    }
078
079    private static Optional<String> getOptionalGroup(Matcher matcher, int index) {
080        String group = matcher.group(index);
081        if (StringUtils.isNotEmpty(group)) {
082            return Optional.of(group);
083        } else {
084            return Optional.empty();
085        }
086    }
087
088    JavadocReference(
089            Optional<String> moduleName,
090            Optional<String> packageNameClassName,
091            Optional<String> member,
092            Optional<String> label) {
093        this.moduleName = moduleName;
094        this.packageNameClassName = packageNameClassName;
095        this.member = member;
096        this.label = label;
097    }
098
099    public Optional<String> getModuleName() {
100        return moduleName;
101    }
102
103    /**
104     *
105     * @return a package name, a class name or a package name followed by a class name
106     */
107    public Optional<String> getPackageNameClassName() {
108        return packageNameClassName;
109    }
110
111    public Optional<String> getMember() {
112        return member;
113    }
114
115    public Optional<String> getLabel() {
116        return label;
117    }
118
119    @Override
120    public String toString() {
121        return "JavadocReference [moduleName=" + moduleName + ", packageNameClassName=" + packageNameClassName
122                + ", member=" + member + ", label=" + label + "]";
123    }
124
125    @Override
126    public int hashCode() {
127        return Objects.hash(label, member, packageNameClassName, moduleName);
128    }
129
130    @Override
131    public boolean equals(Object obj) {
132        if (this == obj) {
133            return true;
134        }
135        if (obj == null) {
136            return false;
137        }
138        if (getClass() != obj.getClass()) {
139            return false;
140        }
141        JavadocReference other = (JavadocReference) obj;
142        return Objects.equals(label, other.label)
143                && Objects.equals(member, other.member)
144                && Objects.equals(packageNameClassName, other.packageNameClassName)
145                && Objects.equals(moduleName, other.moduleName);
146    }
147}