/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jackrabbit.core.persistence.util;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.UUID;
/**
* This utility class can dump the contents of a node bundle. This class is
* based on BundleReader, but is able to dump even if the data is corrupt
* (unlike the BundleReader). The class does not have any dependencies so it can
* be run from the command line without problems (without having to add any jar
* files to the classpath).
*/
public class BundleDumper {
private static final int VERSION_1 = 1;
private static final int VERSION_2 = 2;
private static final int VERSION_3 = 3;
private static final int BINARY_IN_BLOB_STORE = -1;
private static final int BINARY_IN_DATA_STORE = -2;
private static final char[] HEX = "0123456789abcdef".toCharArray();
public static final int UNDEFINED = 0, STRING = 1, BINARY = 2, LONG = 3, DOUBLE = 4, DATE = 5, BOOLEAN = 6,
NAME = 7, PATH = 8, REFERENCE = 9, WEAKREFERENCE = 10, URI = 11, DECIMAL = 12;
private static final String[] NAMES = { "undefined", "String", "Binary", "Long", "Double", "Date", "Boolean",
"Name", "Path", "Reference", "WeakReference", "URI", "Decimal" };
private StringBuilder buffer = new StringBuilder();
/**
* Pre-calculated {@link TimeZone} objects for common timezone offsets.
*/
private static final TimeZone[] COMMON_TIMEZONES = {
TimeZone.getTimeZone("GMT+00:00"), // 0b00000
TimeZone.getTimeZone("GMT+01:00"), // 0b00001
TimeZone.getTimeZone("GMT+02:00"), // 0b00010
TimeZone.getTimeZone("GMT+03:00"), // 0b00011
TimeZone.getTimeZone("GMT+04:00"), // 0b00100
TimeZone.getTimeZone("GMT+05:00"), // 0b00101
TimeZone.getTimeZone("GMT+06:00"), // 0b00110
TimeZone.getTimeZone("GMT+07:00"), // 0b00111
TimeZone.getTimeZone("GMT+08:00"), // 0b01000
TimeZone.getTimeZone("GMT+09:00"), // 0b01001
TimeZone.getTimeZone("GMT+10:00"), // 0b01010
TimeZone.getTimeZone("GMT+11:00"), // 0b01011
TimeZone.getTimeZone("GMT+12:00"), // 0b01100
TimeZone.getTimeZone("GMT+13:00"), // 0b01101
TimeZone.getTimeZone("GMT+14:00"), // 0b01110
TimeZone.getTimeZone("GMT+15:00"), // 0b01111
TimeZone.getTimeZone("GMT-16:00"), // 0b10000
TimeZone.getTimeZone("GMT-15:00"), // 0b10001
TimeZone.getTimeZone("GMT-14:00"), // 0b10010
TimeZone.getTimeZone("GMT-13:00"), // 0b10011
TimeZone.getTimeZone("GMT-12:00"), // 0b10100
TimeZone.getTimeZone("GMT-11:00"), // 0b10101
TimeZone.getTimeZone("GMT-10:00"), // 0b10110
TimeZone.getTimeZone("GMT-09:00"), // 0b10111
TimeZone.getTimeZone("GMT-08:00"), // 0b11000
TimeZone.getTimeZone("GMT-07:00"), // 0b11001
TimeZone.getTimeZone("GMT-06:00"), // 0b11010
TimeZone.getTimeZone("GMT-05:00"), // 0b11011
TimeZone.getTimeZone("GMT-04:00"), // 0b11100
TimeZone.getTimeZone("GMT-03:00"), // 0b11101
TimeZone.getTimeZone("GMT-02:00"), // 0b11110
TimeZone.getTimeZone("GMT-01:00"), // 0b11111
};
/**
* Wrapper for reading structured data from the input stream.
*/
private DataInputStream in;
private int version;
private final String[] namespaces =
// NOTE: The length of this array must be seven
{ "", null, null, null, null, null, null };
static final UUID NULL_PARENT_ID =
UUID.fromString("bb4e9d10-d857-11df-937b-0800200c9a66");
public static void main(String... args) throws IOException {
new BundleDumper().run(args);
}
void run(String... args) throws IOException {
if (args.length < 1) {
System.out.println("Usage: java " + getClass().getName() + " <fileName>");
System.out.println("where the file name points to a node bundle.");
return;
}
RandomAccessFile f = new RandomAccessFile(args[0], "r");
byte[] bundle = new byte[(int) f.length()];
f.readFully(bundle);
f.close();
System.out.println(dump(bundle));
}
public String dump(byte[] bundle) throws IOException {
try {
ByteArrayInputStream bin = new ByteArrayInputStream(bundle);
this.in = new DataInputStream(bin);
version = in.readUnsignedByte();
buffer.append("version: ").append(version).append("\n");
if (version >= VERSION_3) {
readBundleNew();
} else {
readBundleOld();
}
} catch (Exception e) {
buffer.append("\n");
buffer.append("error: ").append(e.toString());
}
return buffer.toString();
}
private void readBundleNew() throws IOException {
// node type
buffer.append("nodeTypeName: ").append(readName()).append("\n");
// parentUUID
UUID parentId = readNodeId();
buffer.append("parentId: ").append(parentId).append("\n");
if (NULL_PARENT_ID.equals(parentId)) {
parentId = null;
buffer.append("parentId is null\n");
}
// read modcount
buffer.append("modCount: ").append((short) readVarInt()).append("\n");
int b = in.readUnsignedByte();
buffer.append("referenceable: ").append((b & 1) != 0).append("\n");
// mixin types
int mn = readVarInt((b >> 7) & 1, 1);
if (mn == 1) {
buffer.append("mixing type:").append(readName()).append("\n");
} else if (mn > 1) {
buffer.append("mixing type count:").append(mn).append("\n");
for (int i = 0; i < mn; i++) {
buffer.append("mixing type:").append(readName()).append("\n");
}
}
// properties
int pn = readVarInt((b >> 4) & 7, 7);
for (int i = 0; i < pn; i++) {
buffer.append("property: ").append(readName()).append("\n");
readPropertyEntry();
}
// child nodes (list of name/uuid pairs)
int nn = readVarInt((b >> 2) & 3, 3);
for (int i = 0; i < nn; i++) {
buffer.append("child node: ").append(readQName()).
append(" id: ").append(readNodeId()).append("\n");
}
// read shared set
int sn = readVarInt((b >> 1) & 1, 1);
if (sn == 1) {
buffer.append("shared set:").append(readNodeId()).append("\n");
} else if (sn > 1) {
buffer.append("shared set count:").append(sn).append("\n");
for (int i = 0; i < sn; i++) {
buffer.append("shared set:").append(readNodeId()).append("\n");
}
}
}
private void readBundleOld() throws IOException {
// read primary type...special handling
int a = in.readUnsignedByte();
int b = in.readUnsignedByte();
int c = in.readUnsignedByte();
String uri = "#" + (a << 16 | b << 8 | c);
String local = "#" + in.readInt();
buffer.append("nodeTypeName: ").append(uri).append(":").append(local).append("\n");
// parentUUID
buffer.append("parentUUID: ").append(readNodeId()).append("\n");
// definitionId
buffer.append("definitionId: ").append(in.readUTF()).append("\n");
// mixin types
String name = readIndexedQName();
if (name != null) {
do {
buffer.append("mixin: ").append(name).append("\n");
name = readIndexedQName();
} while (name != null);
} else {
buffer.append("mixins: -\n");
}
// properties
name = readIndexedQName();
while (name != null) {
buffer.append("property: ").append(name).append("\n");
readPropertyEntry();
name = readIndexedQName();
}
// set referenceable flag
buffer.append("referenceable: ").append(in.readBoolean()).append("\n");
// child nodes (list of uuid/name pairs)
UUID childId = readNodeId();
while (childId != null) {
buffer.append("childId: ").append(childId).append(" ").append(readQName()).append("\n");
childId = readNodeId();
}
// read modcount, since version 1.0
if (version >= VERSION_1) {
buffer.append("modCount: ").append(in.readShort()).append("\n");
}
// read shared set, since version 2.0
if (version >= VERSION_2) {
// shared set (list of parent uuids)
UUID parentId = readNodeId();
if (parentId != null) {
do {
buffer.append("shared set parentId: ").append(parentId).append("\n");
parentId = readNodeId();
} while (parentId != null);
}
}
}
private static String getType(int type) {
try {
return NAMES[type];
} catch (Exception e) {
return "unknown type " + type;
}
}
/**
* Deserializes a <code>PropertyState</code> from the data input stream.
*
* @param id the property id for the new property entry
* @return the property entry
* @throws IOException if an I/O error occurs.
*/
private void readPropertyEntry()
throws IOException {
int count = 1;
int type;
if (version >= VERSION_3) {
int b = in.readUnsignedByte();
type = b & 0x0f;
buffer.append(" type: ").append(getType(type)).append("\n");
int len = b >>> 4;
if (len != 0) {
buffer.append(" multivalued\n");
if (len == 0x0f) {
count = readVarInt() + 0x0f - 1;
} else {
count = len - 1;
}
}
buffer.append(" modcount: ").append((short) readVarInt()).append("\n");
} else {
// type and modcount
type = in.readInt();
buffer.append(" modcount: ").append((short) ((type >> 16) & 0x0ffff)).append("\n");
type &= 0x0ffff;
buffer.append(" type: ").append(getType(type)).append("\n");
// multiValued
boolean mv = in.readBoolean();
if (mv) {
buffer.append(" multivalued\n");
}
// definitionId
buffer.append(" definitionId: ").append(in.readUTF()).append("\n");
// count
count = in.readInt();
if (count != 1) {
buffer.append(" count: ").append(count).append("\n");
}
}
// values
for (int i = 0; i < count; i++) {
switch (type) {
case BINARY:
int size = in.readInt();
if (size == BINARY_IN_DATA_STORE) {
buffer.append(" value: binary in datastore: ").append(readString()).append("\n");
} else if (size == BINARY_IN_BLOB_STORE) {
buffer.append(" value: binary in blobstore: ").append(readString()).append("\n");
} else {
// short values into memory
byte[] data = new byte[size];
in.readFully(data);
buffer.append(" value: binary: ").append(convertBytesToHex(data)).append("\n");
}
break;
case DOUBLE:
buffer.append(" value: double: ").append(in.readDouble()).append("\n");
break;
case DECIMAL:
buffer.append(" value: double: ").append(readDecimal()).append("\n");
break;
case LONG:
if (version >= VERSION_3) {
buffer.append(" value: varLong: ").append(readVarLong()).append("\n");
} else {
buffer.append(" value: long: ").append(in.readLong()).append("\n");
}
break;
case BOOLEAN:
buffer.append(" value: boolean: ").append(in.readBoolean()).append("\n");
break;
case NAME:
buffer.append(" value: name: ").append(readQName()).append("\n");
break;
case WEAKREFERENCE:
buffer.append(" value: weakreference: ").append(readNodeId()).append("\n");
break;
case REFERENCE:
buffer.append(" value: reference: ").append(readNodeId()).append("\n");
break;
case DATE:
if (version >= VERSION_3) {
buffer.append(" value: date: ").append(readDate()).append("\n");
break;
} // else fall through
default:
if (version >= VERSION_3) {
buffer.append(" value: string: ").append(readString()).append("\n");
} else {
// because writeUTF(String) has a size limit of 64k,
// Strings are serialized as <length><byte[]>
int len = in.readInt();
byte[] bytes = new byte[len];
in.readFully(bytes);
buffer.append(" value: string: ").append(new String(bytes, "UTF-8")).append("\n");
}
}
}
}
/**
* Deserializes a node identifier
*
* @return the node id
* @throws IOException in an I/O error occurs.
*/
private UUID readNodeId() throws IOException {
if (version >= VERSION_3 || in.readBoolean()) {
long msb = in.readLong();
long lsb = in.readLong();
return new UUID(msb, lsb);
} else {
return null;
}
}
/**
* Deserializes a BigDecimal
*
* @return the decimal
* @throws IOException in an I/O error occurs.
*/
private BigDecimal readDecimal() throws IOException {
if (in.readBoolean()) {
// TODO more efficient serialization format
return new BigDecimal(readString());
} else {
return null;
}
}
/**
* Deserializes a Name
*
* @return the qname
* @throws IOException in an I/O error occurs.
*/
private String readQName() throws IOException {
if (version >= VERSION_3) {
return readName();
}
String uri = "#" + in.readInt();
String local = in.readUTF();
return uri + ":" + local;
}
/**
* Deserializes an indexed Name
*
* @return the qname
* @throws IOException in an I/O error occurs.
*/
private String readIndexedQName() throws IOException {
if (version >= VERSION_3) {
return readName();
}
int index = in.readInt();
if (index < 0) {
return null;
} else {
String uri = "#" + index;
String local = "#" + in.readInt();
return uri + ":" + local;
}
}
/**
* Deserializes a name written using bundle serialization version 3.
*
* @return deserialized name
* @throws IOException if an I/O error occurs
*/
private String readName() throws IOException {
int b = in.readUnsignedByte();
if ((b & 0x80) == 0) {
return "indexToName #" + b;
} else {
String uri;
int ns = (b >> 4) & 0x07;
if (ns < namespaces.length && namespaces[ns] != null) {
uri = namespaces[ns];
} else {
uri = readString();
if (ns < namespaces.length) {
namespaces[ns] = uri;
}
}
String local = new String(readBytes((b & 0x0f) + 1, 0x10), "UTF-8");
return uri + ":" + local;
}
}
/**
* Deserializes a variable-length integer written using bundle
* serialization version 3.
*
* @return deserialized integer
* @throws IOException if an I/O error occurs
*/
private int readVarInt() throws IOException {
int b = in.readUnsignedByte();
if ((b & 0x80) == 0) {
return b;
} else {
return readVarInt() << 7 | b & 0x7f;
}
}
private int readVarInt(int value, int base) throws IOException {
if (value < base) {
return value;
} else {
return readVarInt() + base;
}
}
/**
* Deserializes a variable-length long written using bundle
* serialization version 3.
*
* @return deserialized long
* @throws IOException if an I/O error occurs
*/
private long readVarLong() throws IOException {
long value = 0;
int bits = 0;
long b;
do {
b = in.readUnsignedByte();
if (bits < 57) {
value = (b & 0x7f) << 57 | value >>> 7;
bits += 7;
} else {
value = (b & 0x01) << 63 | value >>> 1;
bits = 64;
}
} while ((b & 0x80) != 0);
value = value >>> (64 - bits);
if ((value & 1) != 0) {
return ~(value >>> 1);
} else {
return value >>> 1;
}
}
/**
* Deserializes a specially encoded date written using bundle
* serialization version 3.
*
* @return deserialized date
* @throws IOException if an I/O error occurs
*/
private Calendar readDate() throws IOException {
long ts = readVarLong();
TimeZone tz;
if ((ts & 1) == 0) {
tz = COMMON_TIMEZONES[0];
ts >>= 1;
} else if ((ts & 2) == 0) {
tz = COMMON_TIMEZONES[((int) ts >> 2) & 0x1f]; // 5 bits;
ts >>= 7;
} else {
int m = ((int) ts << 19) >> 21; // 11 bits, sign-extended
int h = m / 60;
String s;
if (m < 0) {
s = String.format("GMT-%02d:%02d", -h, h * 60 - m);
} else {
s = String.format("GMT+%02d:%02d", h, m - h * 60);
}
tz = TimeZone.getTimeZone(s);
ts >>= 13;
}
int u = 0;
int s = 0;
int m = 0;
int h = 0;
int type = (int) ts & 3;
ts >>= 2;
switch (type) {
case 3:
u = (int) ts & 0x3fffffff; // 30 bits
s = u / 1000;
m = s / 60;
h = m / 60;
m -= h * 60;
s -= (h * 60 + m) * 60;
u -= ((h * 60 + m) * 60 + s) * 1000;
ts >>= 30;
break;
case 2:
m = (int) ts & 0x07ff; // 11 bits
h = m / 60;
m -= h * 60;
ts >>= 11;
break;
case 1:
h = (int) ts & 0x1f; // 5 bits
ts >>= 5;
break;
}
int d = (int) ts & 0x01ff; // 9 bits;
ts >>= 9;
int y = (int) (ts + 2010);
Calendar value = Calendar.getInstance(tz);
if (y <= 0) {
value.set(Calendar.YEAR, 1 - y);
value.set(Calendar.ERA, GregorianCalendar.BC);
} else {
value.set(Calendar.YEAR, y);
value.set(Calendar.ERA, GregorianCalendar.AD);
}
value.set(Calendar.DAY_OF_YEAR, d);
value.set(Calendar.HOUR_OF_DAY, h);
value.set(Calendar.MINUTE, m);
value.set(Calendar.SECOND, s);
value.set(Calendar.MILLISECOND, u);
return value;
}
private String readString() throws IOException {
if (version >= VERSION_3) {
return new String(readBytes(0, 0), "UTF-8");
} else {
return in.readUTF();
}
}
private byte[] readBytes(int len, int base) throws IOException {
byte[] bytes = new byte[readVarInt(len, base)];
in.readFully(bytes);
return bytes;
}
public static String convertBytesToHex(byte[] value) {
int len = value.length;
char[] buff = new char[len + len];
char[] hex = HEX;
for (int i = 0; i < len; i++) {
int c = value[i] & 0xff;
buff[i + i] = hex[c >> 4];
buff[i + i + 1] = hex[c & 0xf];
}
return new String(buff);
}
}