blob: 0c66a09b414039ff403682e59ae8ae484e6ea9f5 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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.android.builder.internal.incremental;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.io.Closer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
*
* Stores a collection of {@link DependencyData}.
*
* The format is binary and follows the following format:
*
* (Header Tag)(version number: int)
* (Start Tag)(Main File)[(2ndary Tag)(2ndary File)...][(Output tag)(output file)...][(2ndary Output tag)(output file)...]
* (Start Tag)(Main File)[(2ndary Tag)(2ndary File)...][(Output tag)(output file)...][(2ndary Output tag)(output file)...]
* ...
*
* All files are written as (size in int)(byte array, using UTF8 encoding).
*/
public class DependencyDataStore {
private static final byte TAG_HEADER = 0x7F;
private static final byte TAG_START = 0x70;
private static final byte TAG_2NDARY_FILE = 0x71;
private static final byte TAG_OUTPUT = 0x73;
private static final byte TAG_2NDARY_OUTPUT = 0x74;
private static final byte TAG_END = 0x77;
private static final int CURRENT_VERSION = 1;
private final Map<String, DependencyData> mMainFileMap = Maps.newHashMap();
public DependencyDataStore() {
}
public void addData(@NonNull List<DependencyData> dataList) {
for (DependencyData data : dataList) {
mMainFileMap.put(data.getMainFile(), data);
}
}
public void addData(@NonNull DependencyData data) {
mMainFileMap.put(data.getMainFile(), data);
}
public void remove(@NonNull DependencyData data) {
mMainFileMap.remove(data.getMainFile());
}
public void updateAll(@NonNull List<DependencyData> dataList) {
for (DependencyData data : dataList) {
mMainFileMap.put(data.getMainFile(), data);
}
}
@NonNull
public Collection<DependencyData> getData() {
return mMainFileMap.values();
}
@VisibleForTesting
DependencyData getByMainFile(String path) {
return mMainFileMap.get(path);
}
/**
* Returns the map of data using the main file as key.
*
* @see com.android.builder.internal.incremental.DependencyData#getMainFile()
*/
@NonNull
public Map<String, DependencyData> getMainFileMap() {
return mMainFileMap;
}
/**
* Saves the dependency data to a given file.
*
* @param file the file to save the data to.
* @throws IOException
*/
public void saveTo(@NonNull File file) throws IOException {
Closer closer = Closer.create();
try {
FileOutputStream fos = closer.register(new FileOutputStream(file));
fos.write(TAG_HEADER);
writeInt(fos, CURRENT_VERSION);
for (DependencyData data : getData()) {
fos.write(TAG_START);
writePath(fos, data.getMainFile());
for (String path : data.getSecondaryFiles()) {
fos.write(TAG_2NDARY_FILE);
writePath(fos, path);
}
for (String path : data.getOutputFiles()) {
fos.write(TAG_OUTPUT);
writePath(fos, path);
}
for (String path : data.getSecondaryOutputFiles()) {
fos.write(TAG_2NDARY_OUTPUT);
writePath(fos, path);
}
}
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
private static class ReusableBuffer {
byte[] intBuffer = new byte[4];
byte[] pathBuffer = null;
}
/**
* Loads the dependency data from the given file.
*
* @param file the file to load the data from.
* @return a map of file-> list of impacted dependency data.
* @throws IOException
*/
public Multimap<String, DependencyData> loadFrom(@NonNull File file) throws IOException {
Multimap<String, DependencyData> inputMap = ArrayListMultimap.create();
Closer closer = Closer.create();
FileInputStream fis = closer.register(new FileInputStream(file));
// reusable buffer
ReusableBuffer buffers = new ReusableBuffer();
// read the header
if (readByte(fis, buffers) != TAG_HEADER) {
throw new IllegalStateException("Wrong first byte on " + file.getAbsolutePath());
}
int version = readInt(fis, buffers);
if (version != CURRENT_VERSION) {
throw new IOException("Unsupported file version: " + version);
}
try {
// just read the first byte since it should be the TAG_START
byte currentTag = readByte(fis, buffers);
if (currentTag != TAG_START) {
throw new IllegalStateException("Wrong first tag on " + file.getAbsolutePath());
}
DependencyData currentData = new DependencyData();
while (currentTag != TAG_END) {
// read the path
String path = readPath(fis, buffers);
switch (currentTag) {
case TAG_START:
currentData.setMainFile(path);
mMainFileMap.put(path, currentData);
inputMap.put(path, currentData);
break;
case TAG_2NDARY_FILE:
currentData.addSecondaryFile(path);
inputMap.put(path, currentData);
break;
case TAG_OUTPUT:
currentData.addOutputFile(path);
break;
case TAG_2NDARY_OUTPUT:
currentData.addSecondaryOutputFile(path);
break;
}
// read the next tag.
currentTag = readByte(fis, buffers);
if (currentTag == TAG_START) {
currentData = new DependencyData();
}
}
return inputMap;
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
private static void writeInt(@NonNull FileOutputStream fos, int value) throws IOException {
ByteBuffer b = ByteBuffer.allocate(4);
b.putInt(value);
fos.write(b.array());
}
private static void writePath(@NonNull FileOutputStream fos, String path) throws IOException {
byte[] pathBytes = path.getBytes(Charsets.UTF_8);
writeInt(fos, pathBytes.length);
fos.write(pathBytes);
}
private static byte readByte(@NonNull FileInputStream fis, @NonNull ReusableBuffer buffers)
throws IOException {
int read = fis.read(buffers.intBuffer, 0, 1);
if (read != 1) {
return TAG_END;
}
return buffers.intBuffer[0];
}
private static int readInt(@NonNull FileInputStream fis, @NonNull ReusableBuffer buffers)
throws IOException {
int read = fis.read(buffers.intBuffer);
// there must always be 4 bytes for the path length
if (read != 4) {
throw new IOException("Failed to read path length");
}
// get the int value.
ByteBuffer b = ByteBuffer.wrap(buffers.intBuffer);
return b.getInt();
}
private static String readPath(@NonNull FileInputStream fis, @NonNull ReusableBuffer buffers)
throws IOException {
int length = readInt(fis, buffers);
if (buffers.pathBuffer == null || buffers.pathBuffer.length < length) {
buffers.pathBuffer = new byte[length];
}
int read = fis.read(buffers.pathBuffer, 0, length);
if (read != length) {
throw new IOException("Failed to read path");
}
return new String(buffers.pathBuffer, 0, length, Charsets.UTF_8);
}
}