package org.mozilla.osmdroid.tileprovider.modules;
import org.apache.http.util.ByteArrayBuffer;
import org.mozilla.mozstumbler.service.core.logging.ClientLog;
import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Created by victorng on 14-10-24.
* <p/>
* This class represents a serializable Tile.
*/
public class SerializableTile {
// Cache tiles locally for 12 hours. Ichanea doesn't update tiles
// more than once a day anyway and this should be good enough to
// enable offline stumbles.
public static final long CACHE_TILE_MS = 60 * 60 * 12 * 1000;
final protected static char[] hexArray = "0123456789abcdef".toCharArray();
private static final String LOG_TAG = LoggerUtil.makeLogTag(SerializableTile.class);
final byte[] FILE_HEADER = {(byte) 0xde, (byte) 0xca, (byte) 0xfb, (byte) 0xad};
byte[] tData = new byte[0];
Map<String, String> headers = new HashMap<String, String>();
private File myFile;
public SerializableTile(File sTileFile) {
fromFile(sTileFile);
}
public SerializableTile(byte[] tileBytes, String etag) {
setTileData(tileBytes);
setHeader("etag", etag);
}
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public byte[] getTileData() {
return (tData == null)? new byte[0] : tData;
}
public void setTileData(byte[] tileData) {
if (tileData == null) {
tileData = new byte[0];
}
tData = tileData;
}
public void clearHeaders() {
headers.clear();
}
public Map<String, String> getHeaders() {
return headers;
}
public void setHeaders(Map<String, String> h) {
headers.clear();
if (h == null) {
return;
}
for (Map.Entry<String, String> entry : h.entrySet()) {
if (entry.getValue() == null || entry.getKey() == null) {
// skip over this
continue;
}
// Always make headers lowercase
headers.put(entry.getKey().toLowerCase(), entry.getValue());
}
}
/*
Write out the tile data as:
Header:
4 byte int header to signify this is a special file
0xde 0xca 0xfb 0xad
4 byte int declaring the number of headers
Each header is written as:
4 byte int declaring length of header name in UTF-8 bytes
4 byte int declaring length of header value in raw bytes
X bytes for the header
Y bytes for the value
4 bytes declaring content body length
M bytes for content body
*/
private byte[] asBytes() throws CharacterCodingException {
ByteArrayBuffer buff = new ByteArrayBuffer(10);
buff.append(FILE_HEADER, 0, FILE_HEADER.length);
buff.append(intAsBytes(headers.size()), 0, 4);
for (Map.Entry<String, String> entry : headers.entrySet()) {
byte[] keyBytes = entry.getKey().getBytes();
byte[] valueBytes = entry.getValue().getBytes();
buff.append(intAsBytes(keyBytes.length), 0, 4);
buff.append(intAsBytes(valueBytes.length), 0, 4);
buff.append(keyBytes, 0, keyBytes.length);
buff.append(valueBytes, 0, valueBytes.length);
}
if (tData == null || tData.length == 0) {
buff.append(intAsBytes(0), 0, 4);
} else {
buff.append(intAsBytes(tData.length), 0, 4);
buff.append(tData, 0, tData.length);
}
return buff.toByteArray();
}
public boolean saveFile(File aFile) {
try {
myFile = aFile;
// Always update cache-control on save
setHeader("cache-control",
Long.toString(CACHE_TILE_MS + System.currentTimeMillis()));
FileOutputStream fos = new FileOutputStream(aFile);
fos.write(this.asBytes());
fos.flush();
fos.close();
return true;
} catch (IOException e) {
ClientLog.w(LOG_TAG, "Error writing SerializableTile to disk");
return false;
}
}
/*
This will try to save the file if a file object is already set.
Generally only used to update a file that was previously loaded from disk.
*/
public boolean saveFile() {
if (myFile != null) {
return saveFile(myFile);
}
return false;
}
/*
Load a tile from a File object.
Returns whether the file was loaded successfully or not
*/
public boolean fromFile(File file) {
if (!file.exists()) {
return false;
}
boolean result;
result = decodeFile(file);
if (!result) {
// Any decode that fails should try to remove the offending file.
if (file.exists()) {
file.delete();
}
}
return result;
}
private boolean decodeFile(File file) {
if (file.length() <= 4) {
// there's no way this is a valid file
return false;
}
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
return false;
}
byte[] arr = new byte[(int) file.length()];
try {
fis.read(arr);
} catch (IOException e) {
ClientLog.e(LOG_TAG, "Error reading file into array.", e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// wont' be able to do anything here anyway
}
}
}
myFile = file.getAbsoluteFile();
return fromBytes(arr);
}
protected boolean fromBytes(byte[] arr) {
byte[] buffer = null;
Charset charsetE = Charset.forName("UTF-8");
CharsetDecoder decoder = charsetE.newDecoder();
// create a byte buffer and wrap the array
ByteBuffer bb = ByteBuffer.wrap(arr);
// read your integers using ByteBuffer's getInt().
// four bytes converted into an integer!
buffer = new byte[4];
bb.get(buffer, 0, 4);
if (!Arrays.equals(buffer, FILE_HEADER)) {
ClientLog.w(LOG_TAG, "Unexpected header in tile file: [" + bytesToHex(buffer) + "]");
return false;
}
// read # of headers
buffer = new byte[4];
try {
return parseTileBytes(buffer, bb);
} catch (RuntimeException re) {
return false;
}
}
/*
java.nio can throw a bunch of j.l.RuntimeExceptions that don't show up as actual I/O
errors and there's no common parent for nio exceptions other
than java.lang.RuntimeException.
*/
private boolean parseTileBytes(byte[] buffer, ByteBuffer bb) {
bb.get(buffer, 0, 4);
int headerCount = ByteBuffer.wrap(buffer).getInt();
headers.clear();
for (int i = 0; i < headerCount; i++) {
buffer = new byte[4];
bb.get(buffer, 0, 4);
int keyLength = ByteBuffer.wrap(buffer).getInt();
buffer = new byte[4];
bb.get(buffer, 0, 4);
int valueLength = ByteBuffer.wrap(buffer).getInt();
String key = null;
String value = null;
if (keyLength > 0) {
buffer = new byte[keyLength];
bb.get(buffer, 0, keyLength);
key = new String(buffer);
}
if (valueLength > 0) {
buffer = new byte[valueLength];
bb.get(buffer, 0, valueLength);
value = new String(buffer);
}
if (key != null && value != null) {
headers.put(key, value);
}
}
// Remaining bytes should equal the content length of our payload.
buffer = new byte[4];
bb.get(buffer, 0, 4);
int contentLength = ByteBuffer.wrap(buffer).getInt();
if (bb.remaining() != contentLength) {
ClientLog.w(LOG_TAG, "Remaining byte count does not match actual[" + bb.remaining() + "] vs expected[" + contentLength + "]");
// Force data to be null on errors.
tData = null;
return false;
}
tData = new byte[contentLength];
bb.get(tData, 0, contentLength);
return true;
}
private byte[] intAsBytes(int integer) {
return ByteBuffer.allocate(4).putInt(integer).array();
}
public long getCacheControl() {
String cc = getHeader("cache-control");
if (cc == null) {
return 0;
} else {
return Long.parseLong(cc);
}
}
public String getHeader(String k) {
return headers.get(k.toLowerCase());
}
public void setHeader(String k, String v) {
headers.put(k.toLowerCase(), v);
}
public String getEtag() {
return getHeader("etag");
}
}