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