/* * Copyright 2012 NGDATA nv * * 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 org.lilyproject.tools.recordrowvisualizer; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NavigableMap; import freemarker.template.TemplateException; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.HConnectionManager; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.HTableInterface; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.util.Bytes; import org.lilyproject.bytes.impl.DataInputImpl; import org.lilyproject.cli.BaseZkCliTool; import org.lilyproject.repository.api.FieldType; import org.lilyproject.repository.api.IdGenerator; import org.lilyproject.repository.api.QName; import org.lilyproject.repository.api.RecordId; import org.lilyproject.repository.api.SchemaId; import org.lilyproject.repository.api.TypeManager; import org.lilyproject.repository.impl.EncodingUtil; import org.lilyproject.repository.impl.FieldFlags; import org.lilyproject.repository.impl.HBaseTypeManager; import org.lilyproject.repository.impl.id.IdGeneratorImpl; import org.lilyproject.repository.impl.id.SchemaIdImpl; import org.lilyproject.util.Version; import org.lilyproject.util.hbase.HBaseTableFactoryImpl; import org.lilyproject.util.hbase.LilyHBaseSchema.RecordCf; import org.lilyproject.util.hbase.LilyHBaseSchema.RecordColumn; import org.lilyproject.util.hbase.LilyHBaseSchema.Table; import org.lilyproject.util.io.Closer; import org.lilyproject.util.zookeeper.StateWatchingZooKeeper; import org.lilyproject.util.zookeeper.ZooKeeperItf; /** * Tool to visualize the HBase-storage structure of a Lily record, in the form * of an HTML page. */ public class RecordRowVisualizer extends BaseZkCliTool { protected Option recordIdOption; protected Option tableOption; protected RecordRow recordRow; protected TypeManager typeMgr; protected ZooKeeperItf zk; @Override protected String getCmdName() { return "lily-record-row-visualizer"; } @Override protected String getVersion() { return Version.readVersion("org.lilyproject", "lily-record-row-visualizer"); } @Override @SuppressWarnings("static-access") public List<Option> getOptions() { List<Option> options = super.getOptions(); recordIdOption = OptionBuilder .withArgName("record-id") .hasArg() .withDescription("A Lily record ID: UUID.something or USER.something") .withLongOpt("record-id") .create("r"); options.add(recordIdOption); tableOption = OptionBuilder .withArgName("table") .hasArg() .withDescription("Repository table name (defaults to record)") .withLongOpt("table") .create("t"); options.add(tableOption); return options; } public static void main(String[] args) { new RecordRowVisualizer().start(args); } @Override public int run(CommandLine cmd) throws Exception { int result = super.run(cmd); if (result != 0) { return result; } String recordIdString = cmd.getOptionValue(recordIdOption.getOpt()); if (recordIdString == null) { System.out.println("Specify record id with -" + recordIdOption.getOpt()); return 1; } String tableName; if (cmd.hasOption(tableOption.getOpt())) { tableName = cmd.getOptionValue(tableOption.getOpt()); } else { tableName = Table.RECORD.name; } IdGenerator idGenerator = new IdGeneratorImpl(); RecordId recordId = idGenerator.fromString(recordIdString); recordRow = new RecordRow(); recordRow.recordId = recordId; // HBase record table Configuration conf = HBaseConfiguration.create(); conf.set("hbase.zookeeper.quorum", zkConnectionString); HTableInterface table = new HTable(conf, tableName); // Type manager zk = new StateWatchingZooKeeper(zkConnectionString, zkSessionTimeout); typeMgr = new HBaseTypeManager(idGenerator, conf, zk, new HBaseTableFactoryImpl(conf)); Get get = new Get(recordId.toBytes()); get.setMaxVersions(); Result row = table.get(get); NavigableMap<byte[], NavigableMap<byte[], NavigableMap<Long,byte[]>>> root = row.getMap(); readColumns(root.get(RecordCf.DATA.bytes)); byte[][] treatedColumnFamilies = { RecordCf.DATA.bytes }; for (byte[] cf : root.keySet()) { if (!isInArray(cf, treatedColumnFamilies)) { recordRow.unknownColumnFamilies.add(Bytes.toString(cf)); } } executeTemplate("recordrow2html.ftl", Collections.<String, Object>singletonMap("row", recordRow), System.out); return 0; } @Override protected void cleanup() { Closer.close(typeMgr); Closer.close(zk); HConnectionManager.deleteAllConnections(true); super.cleanup(); } private boolean isInArray(byte[] key, byte[][] data) { for (byte[] item : data) { if (Arrays.equals(item, key)) { return true; } } return false; } private void readColumns(NavigableMap<byte[], NavigableMap<Long, byte[]>> cf) throws Exception { Fields fields = recordRow.fields; for (Map.Entry<byte[], NavigableMap<Long, byte[]>> column : cf.entrySet()) { byte[] columnKey = column.getKey(); if (columnKey[0] == RecordColumn.DATA_PREFIX) { SchemaId fieldId = new SchemaIdImpl(Arrays.copyOfRange(columnKey, 1, columnKey.length)); for (Map.Entry<Long, byte[]> version : column.getValue().entrySet()) { long versionNr = version.getKey(); byte[] value = version.getValue(); FieldType fieldType = fields.registerFieldType(fieldId, typeMgr); Map<SchemaId, Object> columns = fields.values.get(versionNr); if (columns == null) { columns = new HashMap<SchemaId, Object>(); fields.values.put(versionNr, columns); } Object decodedValue; if (FieldFlags.isDeletedField(value[0])) { decodedValue = Fields.DELETED; } else { decodedValue = fieldType.getValueType().read(new DataInputImpl(EncodingUtil.stripPrefix(value))); } columns.put(fieldId, decodedValue); } } else if (Arrays.equals(columnKey, RecordColumn.DELETED.bytes)) { setSystemField("Deleted", column.getValue(), BOOLEAN_DECODER); } else if (Arrays.equals(columnKey, RecordColumn.NON_VERSIONED_RT_ID.bytes)) { setSystemField("Non-versioned Record Type ID", column.getValue(), new RecordTypeValueDecoder(typeMgr)); } else if (Arrays.equals(columnKey, RecordColumn.NON_VERSIONED_RT_VERSION.bytes)) { setSystemField("Non-versioned Record Type Version", column.getValue(), LONG_DECODER); } else if (Arrays.equals(columnKey, RecordColumn.VERSIONED_RT_ID.bytes)) { setSystemField("Versioned Record Type ID", column.getValue(), new RecordTypeValueDecoder(typeMgr)); } else if (Arrays.equals(columnKey, RecordColumn.VERSIONED_RT_VERSION.bytes)) { setSystemField("Versioned Record Type Version", column.getValue(), LONG_DECODER); } else if (Arrays.equals(columnKey, RecordColumn.VERSIONED_MUTABLE_RT_ID.bytes)) { setSystemField("Versioned-mutable Record Type ID", column.getValue(), new RecordTypeValueDecoder(typeMgr)); } else if (Arrays.equals(columnKey, RecordColumn.VERSIONED_MUTABLE_RT_VERSION.bytes)) { setSystemField("Versioned-mutable Record Type Version", column.getValue(), LONG_DECODER); } else if (Arrays.equals(columnKey, RecordColumn.VERSION.bytes)) { setSystemField("Record Version", column.getValue(), LONG_DECODER); } else { recordRow.unknownColumns.add(Bytes.toString(columnKey)); } } } private void setSystemField(String name, NavigableMap<Long, byte[]> valuesByVersion, ValueDecoder decoder) { SystemFields systemFields = recordRow.systemFields; SystemFields.SystemField systemField = systemFields.getOrCreateSystemField(name); for (Map.Entry<Long, byte[]> entry : valuesByVersion.entrySet()) { systemField.values.put(entry.getKey(), decoder.decode(entry.getValue())); } } public static interface ValueDecoder<T> { T decode(byte[] bytes); } public static class LongValueDecoder implements ValueDecoder<Long> { @Override public Long decode(byte[] bytes) { return Bytes.toLong(bytes); } } public static class BooleanValueDecoder implements ValueDecoder<Boolean> { @Override public Boolean decode(byte[] bytes) { return Bytes.toBoolean(bytes); } } public static class StringValueDecoder implements ValueDecoder<String> { @Override public String decode(byte[] bytes) { return Bytes.toString(bytes); } } public static class Base64ValueDecoder implements ValueDecoder<String> { @Override public String decode(byte[] bytes) { if (bytes == null) { return null; } char[] result = new char[bytes.length * 2]; for (int i = 0; i < bytes.length; i++) { byte ch = bytes[i]; result[2 * i] = Character.forDigit(Math.abs(ch >> 4), 16); result[2 * i + 1] = Character.forDigit(Math.abs(ch & 0x0f), 16); } return new String(result); } } public static class RecordTypeValueDecoder implements ValueDecoder<RecordTypeInfo> { private TypeManager typeManager; public RecordTypeValueDecoder(TypeManager typeManager) { this.typeManager = typeManager; } @Override public RecordTypeInfo decode(byte[] bytes) { SchemaId id = new SchemaIdImpl(bytes); QName name; try { name = typeManager.getRecordTypeById(id, null).getName(); } catch (Exception e) { name = new QName("", "Failure retrieving record type name"); } return new RecordTypeInfo(id, name); } } private static final StringValueDecoder STRING_DECODER = new StringValueDecoder(); private static final BooleanValueDecoder BOOLEAN_DECODER = new BooleanValueDecoder(); private static final LongValueDecoder LONG_DECODER = new LongValueDecoder(); private static final Base64ValueDecoder BASE64_DECODER = new Base64ValueDecoder(); private void executeTemplate(String template, Map<String, Object> variables, OutputStream os) throws IOException, TemplateException { new TemplateRenderer().render(template, variables, os); } }