001/*
002 * Copyright 2010-2024 The jdependency developers.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.vafer.jdependency;
017
018import java.util.HashMap;
019import java.util.HashSet;
020import java.util.Map;
021import java.util.Set;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
026
027/**
028 * A `Clazz` represents the single class identifier inside a classpath.
029 * There is only one `Clazz` per classname. It has incoming and outgoing
030 * edges defining references and dependencies. If there are different
031 * versions found, it collects their sources as ClazzpathUnits.
032 */
033public final class Clazz implements Comparable<Clazz> {
034
035    private final Set<Clazz> dependencies = new HashSet<>();
036    private final Set<Clazz> references = new HashSet<>();
037    private final Map<ClazzpathUnit, String> units = new HashMap<>();
038
039    public static final class ClazzFile {
040        private ClazzpathUnit unit;
041        private String filename;
042
043        public ClazzFile(ClazzpathUnit unit, String filename) {
044            this.unit = unit;
045            this.filename = filename;
046        }
047
048        public ClazzpathUnit getUnit() {
049            return unit;
050        }
051
052        public String getFilename() {
053            return filename;
054        }
055
056        @Override
057        public String toString() {
058            return "ClazzFile{" +
059                    "unit=" + unit +
060                    ", filename='" + filename + '\'' +
061                    '}';
062        }
063    }
064
065    // Usually a class is only in a single file.
066    // When using MultiRelease Jar files this can be multiple files, one for each java release specified.
067    // The default filename is under the key "8".
068    private final Map<String, ClazzFile> classFilenames = new HashMap<>();
069
070    // The name of the class (like "org.vafer.jdependency.Clazz")
071    private final String name;
072
073    public Clazz( final String pName ) {
074        name = pName;
075    }
076
077    private static final Pattern EXTRACT_MULTI_RELEASE_JAVA_VERSION = Pattern.compile("^(?:META-INF[\\/\\\\]versions[\\/\\\\](\\d+)[\\/\\\\])?([^.]+).class$");
078
079    public static final class ParsedFileName {
080        public String className;
081        public String forJava;
082
083        @Override
084        public String toString() {
085            return "ParsedFileName{" +
086                    "className='" + className + '\'' +
087                    ", forJava='" + forJava + '\'' +
088                    '}';
089        }
090    }
091
092    /**
093     * Determine the class name for the provided filename.
094     *
095     * @param pFileName The filename
096     * @return the class name for the provided filename OR null if it is not a .class file.
097     */
098    public static ParsedFileName parseClassFileName(String pFileName) {
099        if (pFileName == null || !pFileName.endsWith(".class")) {
100            return null;// Not a class filename
101        }
102        // foo/bar/Foo.class -> // foo.bar.Foo
103
104        Matcher matcher = EXTRACT_MULTI_RELEASE_JAVA_VERSION.matcher(pFileName);
105        if (!matcher.matches()) {
106            return null;
107        }
108        ParsedFileName result = new ParsedFileName();
109        result.forJava = matcher.group(1);
110        result.className = separatorsToUnix(matcher.group(2)).replace('/', '.');
111
112        if (result.forJava == null || result.forJava.isEmpty()) {
113            if (result.className.contains("-")) {
114                return null;
115            }
116            result.forJava = "8";
117        }
118
119        return result;
120    }
121
122    /**
123     * Determine if the provided filename is the name of a class that is specific for a java version.
124     * @param pFileName The filename to be evaluated
125     * @return true if this is a filename for a specific java version, false if it is not
126     */
127    public static boolean isMultiReleaseClassFile(String pFileName) {
128        if (pFileName == null) {
129            return false;
130        }
131        Matcher matcher = EXTRACT_MULTI_RELEASE_JAVA_VERSION.matcher(pFileName);
132        if (!matcher.matches()) {
133            return false;
134        }
135        return matcher.group(1) != null && !matcher.group(1).isEmpty();
136    }
137
138    /**
139     * Record that this class name can be found at:
140     * @param pUnit The unit in which the class can be found
141     * @param pForJava For which Java version
142     * @param pFileName Under which filename in the jar.
143     */
144    public void addMultiReleaseFile(ClazzpathUnit pUnit, String pForJava, String pFileName) {
145        classFilenames.put(pForJava, new ClazzFile(pUnit, pFileName));
146    }
147
148    public String getName() {
149        return name;
150    }
151
152    public Map<String, ClazzFile> getFileNames() {
153        return classFilenames;
154    }
155
156    public void addClazzpathUnit( final ClazzpathUnit pUnit, final String pDigest ) {
157        units.put(pUnit, pDigest);
158    }
159
160    public void removeClazzpathUnit( final ClazzpathUnit pUnit ) {
161        units.remove(pUnit);
162    }
163
164    public Set<ClazzpathUnit> getClazzpathUnits() {
165        return units.keySet();
166    }
167
168    public Set<String> getVersions() {
169        // System.out.println("clazz:" + name + " units:" + units);
170        return new HashSet<>(units.values());
171    }
172
173
174    public void addDependency( final Clazz pClazz ) {
175        pClazz.references.add(this);
176        dependencies.add(pClazz);
177    }
178
179    public void removeDependency( final Clazz pClazz ) {
180        pClazz.references.remove(this);
181        dependencies.remove(pClazz);
182    }
183
184    public Set<Clazz> getDependencies() {
185        return dependencies;
186    }
187
188
189
190    public Set<Clazz> getReferences() {
191        return references;
192    }
193
194
195    public Set<Clazz> getTransitiveDependencies() {
196        final Set<Clazz> all = new HashSet<>();
197        findTransitiveDependencies(all);
198        return all;
199    }
200
201
202    void findTransitiveDependencies( final Set<? super Clazz> pAll ) {
203
204        for (Clazz clazz : dependencies) {
205            if (!pAll.contains(clazz)) {
206                pAll.add(clazz);
207                clazz.findTransitiveDependencies(pAll);
208            }
209        }
210    }
211
212
213    public boolean equals( final Object pO ) {
214        if (pO.getClass() != Clazz.class) {
215            return false;
216        }
217        final Clazz c = (Clazz) pO;
218        return name.equals(c.name);
219    }
220
221    public int hashCode() {
222        return name.hashCode();
223    }
224
225    public int compareTo( final Clazz pO ) {
226        return name.compareTo(((Clazz) pO).name);
227    }
228
229    public String toString() {
230        return name + " in " + classFilenames;
231    }
232
233}