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}