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; 031import java.util.stream.Stream; 032import java.util.zip.ZipEntry; 033 034import org.apache.commons.io.input.MessageDigestInputStream; 035import org.objectweb.asm.ClassReader; 036import static org.apache.commons.io.FilenameUtils.normalize; 037import static org.apache.commons.io.FilenameUtils.separatorsToUnix; 038 039import org.vafer.jdependency.Clazz.ParsedFileName; 040import org.vafer.jdependency.asm.DependenciesClassAdapter; 041 042import static org.vafer.jdependency.Clazz.parseClassFileName; 043import static org.vafer.jdependency.utils.StreamUtils.asStream; 044 045 046 047public final class Clazzpath { 048 049 private final Set<ClazzpathUnit> units = new HashSet<>(); 050 private final Map<String, Clazz> missing = new HashMap<>(); 051 private final Map<String, Clazz> clazzes = new HashMap<>(); 052 private final boolean versions; 053 054 private abstract static class Resource { 055 public final String fileName; 056 public final String forJava; 057 public final String name; // Class name ! 058 059 Resource( final String pFileName ) { 060 super(); 061 this.fileName = pFileName; 062 ParsedFileName parsedFileName = parseClassFileName(pFileName); 063 forJava = parsedFileName.forJava; 064 name = parsedFileName.className; 065 } 066 067 abstract InputStream getInputStream() throws IOException; 068 } 069 070 private static boolean isValidResourceName( final String pName ) { 071 return pName != null 072 && pName.endsWith(".class") 073 && ( !pName.contains( "-" ) || pName.contains("META-INF/versions/") ); 074 } 075 076 public Clazzpath() { 077 this(false); 078 } 079 080 public Clazzpath( final boolean pVersions ) { 081 versions = pVersions; 082 } 083 084 public boolean removeClazzpathUnit( final ClazzpathUnit pUnit ) { 085 086 final Set<Clazz> unitClazzes = pUnit.getClazzes(); 087 088 for (Clazz clazz : unitClazzes) { 089 clazz.removeClazzpathUnit(pUnit); 090 if (clazz.getClazzpathUnits().size() == 0) { 091 clazzes.remove(clazz.getName()); 092 } 093 } 094 095 return units.remove(pUnit); 096 } 097 098 public ClazzpathUnit addClazzpathUnit( final File pFile ) throws IOException { 099 return addClazzpathUnit(pFile.toPath()); 100 } 101 102 public ClazzpathUnit addClazzpathUnit( final File pFile, final String pId ) throws IOException { 103 return addClazzpathUnit(pFile.toPath(), pId); 104 } 105 106 107 public ClazzpathUnit addClazzpathUnit( final Path pPath ) throws IOException { 108 return addClazzpathUnit(pPath, pPath.toString()); 109 } 110 111 public ClazzpathUnit addClazzpathUnit( final Path pPath, final String pId ) throws IOException { 112 113 final Path path = pPath.toAbsolutePath(); 114 115 if (Files.isRegularFile(path)) { 116 117 return addClazzpathUnit(Files.newInputStream(path), pId); 118 119 } else if (Files.isDirectory(path)) { 120 121 final String prefix = separatorsToUnix(normalize(path.toString() + '/')); 122 123 try (Stream<Path> stream = Files.walk(path)) { 124 Iterable<Resource> resources = stream 125 .filter(Files::isRegularFile) 126 .filter(p -> isValidResourceName(p.getFileName().toString())) 127 .map(p -> (Resource) new Resource(p.toString().substring(prefix.length())) { 128 InputStream getInputStream() throws IOException { 129 return Files.newInputStream(p); 130 } 131 })::iterator; 132 133 return addClazzpathUnit(resources, pId, true); 134 } 135 } 136 137 throw new IllegalArgumentException("neither file nor directory"); 138 } 139 140 public ClazzpathUnit addClazzpathUnit( final InputStream pInputStream, final String pId ) throws IOException { 141 try (final JarInputStream inputStream = new JarInputStream(pInputStream)) { 142 143 Iterable<Resource> resources = asStream(inputStream) 144 .map(ZipEntry::getName) 145 .filter(Clazzpath::isValidResourceName) 146 .map(name -> (Resource) new Resource(name) { 147 InputStream getInputStream() throws IOException { 148 return inputStream; 149 } 150 })::iterator; 151 152 return addClazzpathUnit(resources, pId, false); 153 } 154 } 155 156 private ClazzpathUnit addClazzpathUnit( final Iterable<Resource> resources, final String pId, boolean shouldCloseResourceStream ) throws IOException { 157 158 final Map<String, Clazz> unitClazzes = new HashMap<>(); 159 final Map<String, Clazz> unitDependencies = new HashMap<>(); 160 161 final ClazzpathUnit unit = new ClazzpathUnit(pId, unitClazzes, unitDependencies); 162 163 for (Resource resource : resources) { 164 165 // extract dependencies of clazz 166 InputStream inputStream = resource.getInputStream(); 167 try { 168 final MessageDigest digest = MessageDigest.getInstance("SHA-256"); 169 final MessageDigestInputStream calculatingInputStream = 170 MessageDigestInputStream.builder().setInputStream(inputStream).setMessageDigest(digest).get(); 171 172 if (versions) { 173 inputStream = calculatingInputStream; 174 } 175 176 final DependenciesClassAdapter v = new DependenciesClassAdapter(); 177 new ClassReader(inputStream).accept(v, ClassReader.EXPAND_FRAMES | ClassReader.SKIP_DEBUG); 178 179 // get or create clazz 180 final String clazzName = resource.name; 181 Clazz clazz = getClazz(clazzName); 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 clazz.addMultiReleaseFile(unit, resource.forJava, resource.fileName); 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 Map<String, Clazz> getClazzesMap() { 247 return new TreeMap<>(clazzes); 248 } 249 250 public Set<Clazz> getClashedClazzes() { 251 final Set<Clazz> all = new HashSet<>(); 252 for (Clazz clazz : clazzes.values()) { 253 if (clazz.getClazzpathUnits().size() > 1) { 254 all.add(clazz); 255 } 256 } 257 return all; 258 } 259 260 public Set<Clazz> getMissingClazzes() { 261 return new HashSet<>(missing.values()); 262 } 263 264 public Clazz getClazz( final String pClazzName ) { 265 return clazzes.get(pClazzName); 266 } 267 268 public ClazzpathUnit[] getUnits() { 269 return units.toArray(new ClazzpathUnit[units.size()]); 270 } 271 272}