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.io.File;
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.Map;
024import java.util.Set;
025import java.util.Base64;
026import java.util.TreeMap;
027import java.util.jar.JarInputStream;
028import java.nio.file.Files;
029import java.nio.file.Path;
030import java.security.MessageDigest;
031import java.util.stream.Stream;
032import java.util.zip.ZipEntry;
033
034import org.apache.commons.io.input.MessageDigestInputStream;
035import org.objectweb.asm.ClassReader;
036import static org.apache.commons.io.FilenameUtils.normalize;
037import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
038
039import org.vafer.jdependency.Clazz.ParsedFileName;
040import org.vafer.jdependency.asm.DependenciesClassAdapter;
041
042import static org.vafer.jdependency.Clazz.parseClassFileName;
043import static org.vafer.jdependency.utils.StreamUtils.asStream;
044
045
046
047public final class Clazzpath {
048
049    private final Set<ClazzpathUnit> units = new HashSet<>();
050    private final Map<String, Clazz> missing = new HashMap<>();
051    private final Map<String, Clazz> clazzes = new HashMap<>();
052    private final boolean versions;
053
054    private abstract static class Resource {
055        public final String fileName;
056        public final String forJava;
057        public final String name; // Class name !
058
059        Resource( final String pFileName ) {
060            super();
061            this.fileName = pFileName;
062            ParsedFileName parsedFileName = parseClassFileName(pFileName);
063            forJava = parsedFileName.forJava;
064            name = parsedFileName.className;
065        }
066
067        abstract InputStream getInputStream() throws IOException;
068    }
069
070    private static boolean isValidResourceName( final String pName ) {
071        return pName != null
072            && pName.endsWith(".class")
073            && ( !pName.contains( "-" ) || pName.contains("META-INF/versions/") );
074    }
075
076    public Clazzpath() {
077        this(false);
078    }
079
080    public Clazzpath( final boolean pVersions ) {
081        versions = pVersions;
082    }
083
084    public boolean removeClazzpathUnit( final ClazzpathUnit pUnit ) {
085
086        final Set<Clazz> unitClazzes = pUnit.getClazzes();
087
088        for (Clazz clazz : unitClazzes) {
089            clazz.removeClazzpathUnit(pUnit);
090            if (clazz.getClazzpathUnits().size() == 0) {
091                clazzes.remove(clazz.getName());
092            }
093        }
094
095        return units.remove(pUnit);
096    }
097
098    public ClazzpathUnit addClazzpathUnit( final File pFile ) throws IOException {
099        return addClazzpathUnit(pFile.toPath());
100    }
101
102    public ClazzpathUnit addClazzpathUnit( final File pFile, final String pId ) throws IOException {
103        return addClazzpathUnit(pFile.toPath(), pId);
104    }
105
106
107    public ClazzpathUnit addClazzpathUnit( final Path pPath ) throws IOException {
108        return addClazzpathUnit(pPath, pPath.toString());
109    }
110
111    public ClazzpathUnit addClazzpathUnit( final Path pPath, final String pId ) throws IOException {
112
113        final Path path = pPath.toAbsolutePath();
114
115        if (Files.isRegularFile(path)) {
116
117            return addClazzpathUnit(Files.newInputStream(path), pId);
118
119        } else if (Files.isDirectory(path)) {
120
121            final String prefix = separatorsToUnix(normalize(path.toString() + '/'));
122
123            try (Stream<Path> stream = Files.walk(path)) {
124                Iterable<Resource> resources = stream
125                        .filter(Files::isRegularFile)
126                        .filter(p -> isValidResourceName(p.getFileName().toString()))
127                        .map(p -> (Resource) new Resource(p.toString().substring(prefix.length())) {
128                            InputStream getInputStream() throws IOException {
129                                return Files.newInputStream(p);
130                            }
131                        })::iterator;
132
133                return addClazzpathUnit(resources, pId, true);
134            }
135        }
136
137        throw new IllegalArgumentException("neither file nor directory");
138    }
139
140    public ClazzpathUnit addClazzpathUnit( final InputStream pInputStream, final String pId ) throws IOException {
141        try (final JarInputStream inputStream = new JarInputStream(pInputStream)) {
142
143            Iterable<Resource> resources = asStream(inputStream)
144                .map(ZipEntry::getName)
145                .filter(Clazzpath::isValidResourceName)
146                .map(name -> (Resource) new Resource(name) {
147                    InputStream getInputStream() throws IOException {
148                        return inputStream;
149                    }
150                })::iterator;
151
152           return addClazzpathUnit(resources, pId, false);
153        }
154    }
155
156    private ClazzpathUnit addClazzpathUnit( final Iterable<Resource> resources, final String pId, boolean shouldCloseResourceStream ) throws IOException {
157
158        final Map<String, Clazz> unitClazzes = new HashMap<>();
159        final Map<String, Clazz> unitDependencies = new HashMap<>();
160
161        final ClazzpathUnit unit = new ClazzpathUnit(pId, unitClazzes, unitDependencies);
162
163        for (Resource resource : resources) {
164
165            // extract dependencies of clazz
166            InputStream inputStream = resource.getInputStream();
167            try {
168                final MessageDigest digest = MessageDigest.getInstance("SHA-256");
169                final  MessageDigestInputStream calculatingInputStream =
170                        MessageDigestInputStream.builder().setInputStream(inputStream).setMessageDigest(digest).get();
171
172                if (versions) {
173                    inputStream = calculatingInputStream;
174                }
175
176                final DependenciesClassAdapter v = new DependenciesClassAdapter();
177                new ClassReader(inputStream).accept(v, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_DEBUG);
178
179                // get or create clazz
180                final String clazzName = resource.name;
181                Clazz clazz = getClazz(clazzName);
182                if (clazz == null) {
183                    clazz = missing.get(clazzName);
184
185                    if (clazz != null) {
186                        // already marked missing
187                        clazz = missing.remove(clazzName);
188                    } else {
189                        clazz = new Clazz(clazzName);
190                    }
191                }
192                clazz.addMultiReleaseFile(unit, resource.forJava, resource.fileName);
193                final String d = Base64.getEncoder().encodeToString(digest.digest());
194                clazz.addClazzpathUnit(unit, d);
195
196                /// add to classpath
197                clazzes.put(clazzName, clazz);
198
199                // add to classpath unit
200                unitClazzes.put(clazzName, clazz);
201
202
203                // iterate through all dependencies
204                final Set<String> depNames = v.getDependencies();
205                for (String depName : depNames) {
206
207                    Clazz dep = getClazz(depName);
208
209                    if (dep == null) {
210                        // there is no such clazz yet
211                        dep = missing.get(depName);
212                    }
213
214                    if (dep == null) {
215                        // it is also not recorded to be missing
216                        dep = new Clazz(depName);
217                        // add as missing
218                        missing.put(depName, dep);
219                    }
220
221                    if (dep != clazz) {
222                        // unit depends on dep
223                        unitDependencies.put(depName, dep);
224                        // clazz depends on dep
225                        clazz.addDependency(dep);
226                    }
227                }
228            } catch(java.security.NoSuchAlgorithmException e) {
229                // well, let's pack and go home
230            } finally {
231                if (shouldCloseResourceStream && inputStream != null) {
232                    inputStream.close();
233                }
234            }
235        }
236
237        units.add(unit);
238
239        return unit;
240    }
241
242    public Set<Clazz> getClazzes() {
243        return new HashSet<>(clazzes.values());
244    }
245
246    public Map<String, Clazz> getClazzesMap() {
247        return new TreeMap<>(clazzes);
248    }
249
250    public Set<Clazz> getClashedClazzes() {
251        final Set<Clazz> all = new HashSet<>();
252        for (Clazz clazz : clazzes.values()) {
253            if (clazz.getClazzpathUnits().size() > 1) {
254                all.add(clazz);
255            }
256        }
257        return all;
258    }
259
260    public Set<Clazz> getMissingClazzes() {
261        return new HashSet<>(missing.values());
262    }
263
264    public Clazz getClazz( final String pClazzName ) {
265        return clazzes.get(pClazzName);
266    }
267
268    public ClazzpathUnit[] getUnits() {
269        return units.toArray(new ClazzpathUnit[units.size()]);
270    }
271
272}