/** * Copyright 2007 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tonicsystems.jarjar.transform.jar; import com.tonicsystems.jarjar.transform.config.PatternUtils; import com.tonicsystems.jarjar.transform.config.ClassKeepTransitive; import com.tonicsystems.jarjar.transform.Transformable; import com.tonicsystems.jarjar.util.ClassNameUtils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import org.objectweb.asm.ClassReader; import org.objectweb.asm.commons.Remapper; import org.objectweb.asm.commons.RemappingClassAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Keeps all classes reachable from a given set of roots. * * Put this early in the chain as it does not honour renames. */ public class ClassClosureJarProcessor extends AbstractFilterJarProcessor { private static final Logger LOG = LoggerFactory.getLogger(ClassClosureJarProcessor.class); private static class DependencyCollector extends Remapper { private final Set<String> dependencies = new HashSet<String>(); @Override public String map(String key) { if (key.startsWith("java/") || key.startsWith("javax/")) return null; dependencies.add(key); return null; } @Override public Object mapValue(Object value) { if (value instanceof String) { String s = (String) value; if (ClassNameUtils.isArrayForName(s)) { mapDesc(s.replace('.', '/')); } else if (ClassNameUtils.isForName(s)) { map(s.replace('.', '/')); } return value; } else { return super.mapValue(value); } } } private final List<ClassKeepTransitive> patterns; private final List<String> roots = new ArrayList<String>(); private final Map<String, Set<String>> dependencies = new HashMap<String, Set<String>>(); private Set<String> closure; public ClassClosureJarProcessor(@Nonnull Iterable<? extends ClassKeepTransitive> patterns) { this.patterns = PatternUtils.toList(patterns); } public ClassClosureJarProcessor(@Nonnull ClassKeepTransitive... patterns) { this(Arrays.asList(patterns)); } public void addKeep(@Nonnull ClassKeepTransitive pattern) { patterns.add(pattern); } private boolean isEnabled() { return !patterns.isEmpty(); } @Override public Result scan(Transformable struct) throws IOException { if (!isEnabled()) return Result.KEEP; try { if (ClassNameUtils.isClass(struct.name)) { String name = struct.name.substring(0, struct.name.length() - 6); for (ClassKeepTransitive pattern : patterns) if (pattern.matches(name)) roots.add(name); DependencyCollector collector = new DependencyCollector(); dependencies.put(name, collector.dependencies); new ClassReader(new ByteArrayInputStream(struct.data)).accept(new RemappingClassAdapter(null, collector), ClassReader.EXPAND_FRAMES); collector.dependencies.remove(name); } } catch (Exception e) { LOG.warn("Error reading " + struct.name + ": " + e.getMessage()); } return Result.KEEP; } private void addTransitiveClosure(Collection<? super String> out, Collection<String> itemDependencies) { if (itemDependencies == null) return; for (String name : itemDependencies) if (out.add(name)) addTransitiveClosure(out, dependencies.get(name)); } @Override protected boolean isFiltered(String name) { if (closure == null) { closure = new HashSet<String>(); addTransitiveClosure(closure, roots); } return !closure.contains(name); } @Override public Result process(Transformable struct) throws IOException { if (!isEnabled()) return Result.KEEP; if (!ClassNameUtils.isClass(struct.name)) return Result.KEEP; return super.process(struct); } }