blob: dff7ec94fc69815dbf705be15e1fbc20df0147aa [file] [log] [blame]
/*
* Copyright 2013-present Facebook, 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.facebook.buck.java.abi;
import static javax.lang.model.SourceVersion.RELEASE_7;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
@SupportedSourceVersion(RELEASE_7)
@SupportedAnnotationTypes("*")
@SupportedOptions({AbiWriterProtocol.PARAM_ABI_OUTPUT_FILE})
public class AbiWriter extends AbstractProcessor {
private SortedSet<String> classes = new TreeSet<>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
RenderableTypes factory = new RenderableTypes();
for (Element element : roundEnv.getRootElements()) {
if (element instanceof TypeElement) {
Renderable renderable = factory.deriveFor(element);
StringBuilder builder = new StringBuilder();
renderable.appendTo(builder);
classes.add(builder.toString());
} else if (element instanceof PackageElement) {
// Only found in package-info classes and therefore do not contribute to the ABI.
continue;
} else {
throw new RuntimeException("Unknown type: " + element.getKind());
}
}
String destFile = processingEnv.getOptions().get(AbiWriterProtocol.PARAM_ABI_OUTPUT_FILE);
if (destFile != null) {
writeAbi(new File(destFile));
}
// We're not laying claim to any annotations.
return false;
}
private void writeAbi(File file) {
if (!file.getParentFile().exists()) {
throw new IllegalArgumentException("Directory for ABI key does not exist: " + file);
}
if (file.exists() && !file.delete()) {
throw new IllegalArgumentException("Unable to delete existing ABI key: " + file);
}
String key = computeAbiKey();
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
out.write(key.getBytes());
out.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public SortedSet<String> getSummaries() {
return Collections.unmodifiableSortedSet(classes);
}
/**
* Creates a SHA-1 hash from the ABI information extracted by this {@link AbiWriter}.
*/
public String computeAbiKey() {
SortedSet<String> summaries = getSummaries();
return computeAbiKey(summaries);
}
static String computeAbiKey(SortedSet<String> summaries) {
try {
MessageDigest digest = MessageDigest.getInstance("sha-1");
for (String summary : summaries) {
// "2" is the number of bytes in a java character
ByteBuffer buffer = ByteBuffer.allocate(summary.length() * 2).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < summary.length(); i++) {
buffer.putChar(summary.charAt(i));
}
digest.update(buffer.array());
}
byte[] sha1Bytes = digest.digest();
// This isn't a particularly fast operation. A quick test indicates that it's approximately
// 3-4 times slower than "new BigInteger(1, sha1Bytes).toString(16)". It does, however, ensure
// that the resulting string is always 40 characters long and padded with 0 if necessary.
// To give an indication of speed, on my i7 mbp, 100k string generations takes ~450ms compared
// to ~150ms. In short, the speed hit isn't going to be the end of the world for our use case.
return String.format("%040x", new BigInteger(1, sha1Bytes));
} catch (NoSuchAlgorithmException e) {
// Note: if we get this we're on a broken JRE and we're not having fun.
throw new RuntimeException(e);
}
}
}