001/*
002 * Copyright 2010-2018 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.FileInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.Map;
026import java.util.Set;
027import java.util.jar.JarEntry;
028import java.util.jar.JarInputStream;
029
030import org.apache.commons.io.FileUtils;
031import org.apache.commons.io.FilenameUtils;
032import org.objectweb.asm.ClassReader;
033import org.vafer.jdependency.asm.DependenciesClassAdapter;
034
035public final class Clazzpath {
036
037    private static abstract class Resource {
038        final String name;
039
040        Resource( String pName ) {
041            super();
042            this.name = pName.substring(0, pName.length() - 6).replace('/', '.');
043        }
044
045        abstract InputStream getInputStream() throws IOException;
046
047        static boolean isValidName( String pName ) {
048            return pName != null && pName.endsWith(".class") && !pName.contains( "-" );
049        }
050    }
051
052    private final Set<ClazzpathUnit> units = new HashSet<ClazzpathUnit>();
053    private final Map<String, Clazz> missing = new HashMap<String, Clazz>();
054    private final Map<String, Clazz> clazzes = new HashMap<String, Clazz>();
055
056    public Clazzpath() {
057    }
058
059    public boolean removeClazzpathUnit( final ClazzpathUnit pUnit ) {
060
061        final Set<Clazz> unitClazzes = pUnit.getClazzes();
062
063        for (Clazz clazz : unitClazzes) {
064            clazz.removeClazzpathUnit(pUnit);
065            if (clazz.getClazzpathUnits().size() == 0) {
066                clazzes.remove(clazz.toString());
067                // missing.put(clazz.toString(), clazz);
068            }
069        }
070
071        return units.remove(pUnit);
072    }
073
074    /**
075     * Add a {@link ClazzpathUnit} to this {@link Clazzpath}.
076     * @param pFile may be a directory or a jar file
077     * @return newly created {@link ClazzpathUnit} with id of pFile.absolutePath
078     * @throws IOException
079     */
080    public final ClazzpathUnit addClazzpathUnit( final File pFile ) throws IOException {
081        return addClazzpathUnit(pFile, pFile.getAbsolutePath());
082    }
083
084    public ClazzpathUnit addClazzpathUnit( final File pFile, final String pId ) throws IOException {
085        if ( pFile.isFile() ) {
086            return addClazzpathUnit( new FileInputStream(pFile), pId);
087        }
088        if (pFile.isDirectory()) {
089            final String prefix =
090                FilenameUtils.separatorsToUnix(FilenameUtils
091                    .normalize(new StringBuilder(pFile.getAbsolutePath())
092                        .append(File.separatorChar).toString()));
093            final boolean recursive = true;
094            @SuppressWarnings("unchecked")
095            final Iterator<File> files = FileUtils.iterateFiles(pFile, new String[] { "class" }, recursive);
096            return addClazzpathUnit( new Iterable<Resource>() {
097
098                public Iterator<Resource> iterator() {
099                    return new Iterator<Clazzpath.Resource>() {
100
101                        public boolean hasNext() {
102                            return files.hasNext();
103                        }
104
105                        public Resource next() {
106                            final File file = files.next();
107                            return new Resource(file.getAbsolutePath().substring(prefix.length())) {
108
109                                @Override
110                                InputStream getInputStream() throws IOException {
111                                    return new FileInputStream(file);
112                                }
113                            };
114                        }
115
116                        public void remove() {
117                            throw new UnsupportedOperationException();
118                        }
119                    };
120                }
121
122            }, pId, true);
123        }
124        throw new IllegalArgumentException();
125    }
126
127    public ClazzpathUnit addClazzpathUnit(final InputStream pInputStream, final String pId) throws IOException {
128        final JarInputStream inputStream = new JarInputStream(pInputStream);
129        try {
130            final JarEntry[] entryHolder = new JarEntry[1];
131
132            return addClazzpathUnit(new Iterable<Resource>() {
133
134                public Iterator<Resource> iterator() {
135                    return new Iterator<Resource>() {
136
137                        public boolean hasNext() {
138                            try {
139                                do {
140                                    entryHolder[0] = inputStream.getNextJarEntry();
141                                } while (entryHolder[0] != null && !Resource.isValidName(entryHolder[0].getName()));
142                            } catch (IOException e) {
143                                throw new RuntimeException(e);
144                            }
145                            return entryHolder[0] != null;
146                        }
147
148                        public Resource next() {
149                            return new Resource(entryHolder[0].getName()) {
150
151                                @Override
152                                InputStream getInputStream() {
153                                    return inputStream;
154                                }
155                            };
156                        }
157
158                        public void remove() {
159                            throw new UnsupportedOperationException();
160                        }
161
162                    };
163                }
164            }, pId, false);
165        } finally {
166            inputStream.close();
167        }
168    }
169
170    private ClazzpathUnit addClazzpathUnit(final Iterable<Resource> resources, final String pId, boolean shouldCloseResourceStream) throws IOException {
171        final Map<String, Clazz> unitClazzes = new HashMap<String, Clazz>();
172        final Map<String, Clazz> unitDependencies = new HashMap<String, Clazz>();
173
174        final ClazzpathUnit unit = new ClazzpathUnit(pId, unitClazzes, unitDependencies);
175
176        for (Resource resource : resources) {
177
178            final String clazzName = resource.name;
179
180            Clazz clazz = getClazz(clazzName);
181
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
193            clazz.addClazzpathUnit(unit);
194
195            clazzes.put(clazzName, clazz);
196            unitClazzes.put(clazzName, clazz);
197
198            final DependenciesClassAdapter v = new DependenciesClassAdapter();
199            final InputStream inputStream = resource.getInputStream();
200            try {
201                new ClassReader(inputStream).accept(v, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_DEBUG);
202            } finally {
203                if (shouldCloseResourceStream) inputStream.close();
204            }
205
206            final Set<String> depNames = v.getDependencies();
207
208            for (String depName : depNames) {
209
210                Clazz dep = getClazz(depName);
211
212                if (dep == null) {
213                    // there is no such clazz yet
214                    dep = missing.get(depName);
215                }
216
217                if (dep == null) {
218                    // it is also not recorded to be missing
219                    dep = new Clazz(depName);
220                    dep.addClazzpathUnit(unit);
221                    missing.put(depName, dep);
222                }
223
224                if (dep != clazz) {
225                    unitDependencies.put(depName, dep);
226                    clazz.addDependency(dep);
227                }
228            }
229        }
230
231        units.add(unit);
232
233        return unit;
234    }
235
236    public Set<Clazz> getClazzes() {
237        final Set<Clazz> result = new HashSet<Clazz>(clazzes.values());
238        return result;
239    }
240
241    public Set<Clazz> getClashedClazzes() {
242        final Set<Clazz> all = new HashSet<Clazz>();
243        for (Clazz clazz : clazzes.values()) {
244            if (clazz.getClazzpathUnits().size() > 1) {
245                all.add(clazz);
246            }
247        }
248        return all;
249    }
250
251    public Set<Clazz> getMissingClazzes() {
252        final Set<Clazz> result = new HashSet<Clazz>(missing.values());
253        return result;
254    }
255
256    public Clazz getClazz(final String pClazzName) {
257        final Clazz result = (Clazz) clazzes.get(pClazzName);
258        return result;
259    }
260
261    public ClazzpathUnit[] getUnits() {
262        final ClazzpathUnit[] result = units.toArray(new ClazzpathUnit[units.size()]);
263        return result;
264    }
265
266}