001/*
002 * Copyright 2010-2021 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.jar.JarInputStream;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.security.MessageDigest;
030
031import org.objectweb.asm.ClassReader;
032import org.apache.commons.io.input.MessageDigestCalculatingInputStream;
033import static org.apache.commons.io.FilenameUtils.normalize;
034import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
035
036import org.vafer.jdependency.asm.DependenciesClassAdapter;
037import static org.vafer.jdependency.utils.StreamUtils.asStream;
038
039
040
041public final class Clazzpath {
042
043    private final Set<ClazzpathUnit> units = new HashSet<>();
044    private final Map<String, Clazz> missing = new HashMap<>();
045    private final Map<String, Clazz> clazzes = new HashMap<>();
046    private final boolean versions;
047
048    private abstract static class Resource {
049
050        private static final int ext = ".class".length();
051
052        public final String name;
053
054        Resource( final String pName ) {
055            super();
056
057            final int all = pName.length();
058
059            // foo/bar/Foo.class -> // foo.bar.Foo
060            this.name = separatorsToUnix(pName)
061                .substring(0, all - ext)
062                .replace('/', '.');
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( "-" );
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.toString());
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 MessageDigestCalculatingInputStream calculatingInputStream = new MessageDigestCalculatingInputStream(inputStream, digest);
172
173                if (versions) {
174                    inputStream = calculatingInputStream;
175                }
176
177                final DependenciesClassAdapter v = new DependenciesClassAdapter();
178                new ClassReader(inputStream).accept(v, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_DEBUG);
179
180                // get or create clazz
181                final String clazzName = resource.name;
182                Clazz clazz = getClazz(clazzName);
183                if (clazz == null) {
184                    clazz = missing.get(clazzName);
185
186                    if (clazz != null) {
187                        // already marked missing
188                        clazz = missing.remove(clazzName);
189                    } else {
190                        clazz = new Clazz(clazzName);
191                    }
192                }
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 Set<Clazz> getClashedClazzes() {
247        final Set<Clazz> all = new HashSet<>();
248        for (Clazz clazz : clazzes.values()) {
249            if (clazz.getClazzpathUnits().size() > 1) {
250                all.add(clazz);
251            }
252        }
253        return all;
254    }
255
256    public Set<Clazz> getMissingClazzes() {
257        return new HashSet<>(missing.values());
258    }
259
260    public Clazz getClazz( final String pClazzName ) {
261        return clazzes.get(pClazzName);
262    }
263
264    public ClazzpathUnit[] getUnits() {
265        return units.toArray(new ClazzpathUnit[units.size()]);
266    }
267
268}