/**
* 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);
}
}