001/*
002 * Copyright 2010-2019 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.jar.JarInputStream;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import org.objectweb.asm.ClassReader;
029
030import org.vafer.jdependency.asm.DependenciesClassAdapter;
031
032import static org.apache.commons.io.FilenameUtils.normalize;
033import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
034
035import static org.vafer.jdependency.utils.StreamUtils.asStream;
036
037
038
039public final class Clazzpath {
040
041    private final Set<ClazzpathUnit> units = new HashSet<ClazzpathUnit>();
042    private final Map<String, Clazz> missing = new HashMap<String, Clazz>();
043    private final Map<String, Clazz> clazzes = new HashMap<String, Clazz>();
044
045    private static abstract class Resource {
046
047        final private static int ext = ".class".length();
048
049        final public String name;
050
051        Resource( final String pName ) {
052            super();
053
054            final int all = pName.length();
055
056            // foo/bar/Foo.class -> // foo.bar.Foo
057            this.name = separatorsToUnix(pName)
058                .substring(0, all - ext)
059                .replace('/', '.');
060        }
061
062        abstract InputStream getInputStream() throws IOException;
063    }
064
065    private static boolean isValidResourceName( final String pName ) {
066        return pName != null
067            && pName.endsWith(".class")
068            && !pName.contains( "-" );
069    }
070
071    public boolean removeClazzpathUnit( final ClazzpathUnit pUnit ) {
072
073        final Set<Clazz> unitClazzes = pUnit.getClazzes();
074
075        for (Clazz clazz : unitClazzes) {
076            clazz.removeClazzpathUnit(pUnit);
077            if (clazz.getClazzpathUnits().size() == 0) {
078                clazzes.remove(clazz.toString());
079            }
080        }
081
082        return units.remove(pUnit);
083    }
084
085    public ClazzpathUnit addClazzpathUnit( final File pFile ) throws IOException {
086        return addClazzpathUnit(pFile.toPath());
087    }
088
089    public ClazzpathUnit addClazzpathUnit( final File pFile, final String pId ) throws IOException {
090        return addClazzpathUnit(pFile.toPath(), pId);
091    }
092
093
094    public ClazzpathUnit addClazzpathUnit( final Path pPath ) throws IOException {
095        return addClazzpathUnit(pPath, pPath.toString());
096    }
097
098    public ClazzpathUnit addClazzpathUnit( final Path pPath, final String pId ) throws IOException {
099
100        final Path path = pPath.toAbsolutePath();
101
102        if (Files.isRegularFile(path)) {
103
104            return addClazzpathUnit(Files.newInputStream(path), pId);
105
106        } else if (Files.isDirectory(path)) {
107
108            final String prefix = separatorsToUnix(normalize(path.toString() + '/'));
109
110            Iterable<Resource> resources = Files.walk(path)
111                .filter(p -> Files.isRegularFile(p))
112                .filter(p -> isValidResourceName(p.getFileName().toString()))
113                .map(p -> (Resource) new Resource(p.toString().substring(prefix.length())) {
114                    InputStream getInputStream() throws IOException {
115                        return Files.newInputStream(p);
116                    }
117                })::iterator;
118
119            return addClazzpathUnit(resources, pId, true);
120        }
121
122        throw new IllegalArgumentException("neither file nor directory");
123    }
124
125    public ClazzpathUnit addClazzpathUnit( final InputStream pInputStream, final String pId ) throws IOException {
126
127        final JarInputStream inputStream = new JarInputStream(pInputStream);
128
129        try {
130
131            Iterable<Resource> resources = asStream(inputStream)
132                .map(e -> e.getName())
133                .filter(name -> isValidResourceName(name))
134                .map(name -> (Resource) new Resource(name) {
135                    InputStream getInputStream() throws IOException {
136                        return inputStream;
137                    }
138                })::iterator;
139
140           return addClazzpathUnit(resources, pId, false);
141
142        } finally {
143            inputStream.close();
144        }
145    }
146
147    private ClazzpathUnit addClazzpathUnit( final Iterable<Resource> resources, final String pId, boolean shouldCloseResourceStream ) throws IOException {
148
149        final Map<String, Clazz> unitClazzes = new HashMap<String, Clazz>();
150        final Map<String, Clazz> unitDependencies = new HashMap<String, Clazz>();
151
152        final ClazzpathUnit unit = new ClazzpathUnit(pId, unitClazzes, unitDependencies);
153
154        for (Resource resource : resources) {
155
156            final String clazzName = resource.name;
157
158            Clazz clazz = getClazz(clazzName);
159
160            if (clazz == null) {
161                clazz = missing.get(clazzName);
162
163                if (clazz != null) {
164                    // already marked missing
165                    clazz = missing.remove(clazzName);
166                } else {
167                    clazz = new Clazz(clazzName);
168                }
169            }
170
171            clazz.addClazzpathUnit(unit);
172
173            clazzes.put(clazzName, clazz);
174            unitClazzes.put(clazzName, clazz);
175
176            final DependenciesClassAdapter v = new DependenciesClassAdapter();
177            final InputStream inputStream = resource.getInputStream();
178            try {
179                new ClassReader(inputStream).accept(v, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_DEBUG);
180            } finally {
181                if (shouldCloseResourceStream) inputStream.close();
182            }
183
184            final Set<String> depNames = v.getDependencies();
185
186            for (String depName : depNames) {
187
188                Clazz dep = getClazz(depName);
189
190                if (dep == null) {
191                    // there is no such clazz yet
192                    dep = missing.get(depName);
193                }
194
195                if (dep == null) {
196                    // it is also not recorded to be missing
197                    dep = new Clazz(depName);
198                    dep.addClazzpathUnit(unit);
199                    missing.put(depName, dep);
200                }
201
202                if (dep != clazz) {
203                    unitDependencies.put(depName, dep);
204                    clazz.addDependency(dep);
205                }
206            }
207        }
208
209        units.add(unit);
210
211        return unit;
212    }
213
214    public Set<Clazz> getClazzes() {
215        return new HashSet<Clazz>(clazzes.values());
216    }
217
218    public Set<Clazz> getClashedClazzes() {
219        final Set<Clazz> all = new HashSet<Clazz>();
220        for (Clazz clazz : clazzes.values()) {
221            if (clazz.getClazzpathUnits().size() > 1) {
222                all.add(clazz);
223            }
224        }
225        return all;
226    }
227
228    public Set<Clazz> getMissingClazzes() {
229        return new HashSet<Clazz>(missing.values());
230    }
231
232    public Clazz getClazz( final String pClazzName ) {
233        return (Clazz) clazzes.get(pClazzName);
234    }
235
236    public ClazzpathUnit[] getUnits() {
237        return units.toArray(new ClazzpathUnit[units.size()]);
238    }
239
240}