/* * Copyright (c) 2014 Oculus Info Inc. * http://www.oculusinfo.com/ * * Released under the MIT License. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.oculusinfo.binning.io.serialization; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.*; import com.oculusinfo.binning.BinIndex; import com.oculusinfo.binning.TileIndex; import com.oculusinfo.binning.TileData; import com.oculusinfo.binning.TileData.StorageType; import com.oculusinfo.factory.util.Pair; import org.apache.avro.Schema; import org.apache.avro.file.CodecFactory; import org.apache.avro.file.DataFileStream; import org.apache.avro.file.DataFileWriter; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericDatumReader; import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.generic.GenericRecord; import org.apache.avro.io.DatumReader; import org.apache.avro.io.DatumWriter; import com.oculusinfo.binning.impl.DenseTileData; import com.oculusinfo.binning.impl.SparseTileData; import com.oculusinfo.binning.util.TypeDescriptor; abstract public class GenericAvroSerializer<T> implements TileSerializer<T> { private static final long serialVersionUID = 5775555328063499845L; // Functions to encode and decode codecs as strings, so we can serialize // them accross the network or accross machines. Unfortunately, we cannot // simply use CodecFactory.toString and CodecFactory.fromString, as they // fail to reverse each other for deflate codecs. private static CodecFactory descriptionToCodec (String codecDescription) { if (codecDescription.startsWith("deflate")) { // Knock off the initial "deflate-" int deflateLevel = Integer.parseInt(codecDescription.substring(8)); return CodecFactory.deflateCodec(deflateLevel); } else { return CodecFactory.fromString(codecDescription); } } private static String codecToDescription (CodecFactory codec) { return codec.toString(); } private transient ThreadLocal<Map<StorageType, Schema>> _tileSchema; private transient Schema _recordSchema; private String _compressionCodec; private TypeDescriptor _typeDescription; protected GenericAvroSerializer (CodecFactory compressionCodec, TypeDescriptor typeDescription) { _compressionCodec = codecToDescription(compressionCodec); _typeDescription = typeDescription; _tileSchema = null; _recordSchema = null; } abstract protected String getRecordSchemaFile (); abstract protected T getValue (GenericRecord bin); abstract protected void setValue (GenericRecord bin, T value) throws IOException ; public String getFileExtension(){ return "avro"; } protected Schema getRecordSchema () throws IOException { if (_recordSchema == null) { _recordSchema = createRecordSchema(); } return _recordSchema; } protected Schema createRecordSchema() throws IOException { return new AvroSchemaComposer().addResource(getRecordSchemaFile()).resolved(); } protected Schema getTileSchema (StorageType storage) throws IOException { if (null == _tileSchema) _tileSchema = new ThreadLocal<Map<StorageType, Schema>>() { @Override protected Map<StorageType, Schema> initialValue () { return new HashMap<TileData.StorageType, Schema>(); } }; if (!_tileSchema.get().containsKey(storage)) { _tileSchema.get().put(storage, createTileSchema(storage)); } return _tileSchema.get().get(storage); } protected Schema createTileSchema (StorageType storage) throws IOException { switch (storage) { case Dense: return new AvroSchemaComposer().add(getRecordSchema()).addResource("denseTile.avsc").resolved(); case Sparse: return new AvroSchemaComposer().add(getRecordSchema()).addResource("sparseTile.avsc").resolved(); default: return null; } } @Override public TypeDescriptor getBinTypeDescription () { return _typeDescription; } protected Map<String, String> getTileMetaData (TileData<T> tile) { Collection<String> keys = tile.getMetaDataProperties(); if (null == keys || keys.isEmpty()) return null; Map<String, String> metaData = new HashMap<String, String>(); for (String key: keys) { String value = tile.getMetaData(key); if (null != value) metaData.put(key, value); } return metaData; } @Override public TileData<T> deserialize(TileIndex index, InputStream stream) throws IOException { DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>(); DataFileStream<GenericRecord> dataFileReader = new DataFileStream<GenericRecord>(stream, reader); try { GenericRecord r = dataFileReader.next(); int level = (Integer) r.get("level"); int xIndex = (Integer) r.get("xIndex"); int yIndex = (Integer) r.get("yIndex"); int xBins = (Integer) r.get("xBinCount"); int yBins = (Integer) r.get("yBinCount"); Map<?, ?> meta = (Map<?, ?>) r.get("meta"); TileIndex newTileIndex = new TileIndex(level, xIndex, yIndex, xBins, yBins); // Warning suppressed because Array.newInstance definitionally returns // something of the correct type, or throws an exception @SuppressWarnings("unchecked") GenericData.Array<GenericRecord> bins = (GenericData.Array<GenericRecord>) r.get("values"); // See if this is a sparse or dense array. StorageType storage = StorageType.Dense; if (r.getSchema().getName().equals("sparseTile")) { storage = StorageType.Sparse; } TileData<T> newTile = null; switch (storage) { case Dense: { List<T> data = new ArrayList<T>(xBins * yBins); int i = 0; for (GenericRecord bin : bins) { data.add(getValue(bin)); ++i; if (i >= xBins * yBins) break; } T defaultValue = null; GenericRecord defaultBin = (GenericRecord) r.get("default"); if (null != defaultBin) { defaultValue = getValue((GenericRecord) r.get("default")); } newTile = new DenseTileData<T>(newTileIndex, defaultValue, data); break; } case Sparse: { Map<Integer, Map<Integer, T>> data = new HashMap<>(); for (GenericRecord bin : bins) { int x = (Integer) (bin.get("xIndex")); int y = (Integer) (bin.get("yIndex")); T value = getValue((GenericRecord) bin.get("value")); if (!data.containsKey(x)) data.put(x, new HashMap<Integer, T>()); data.get(x).put(y, value); } T defaultValue = getValue((GenericRecord) r.get("default")); newTile = new SparseTileData<T>(newTileIndex, data, defaultValue); break; } default: return null; } // Add in metaData if (null != meta) { for (Object key : meta.keySet()) { if (null != key) { Object value = meta.get(key); if (null != value) { newTile.setMetaData(key.toString(), value.toString()); } } } } return newTile; } finally { dataFileReader.close(); stream.close(); } } @Override public void serialize (TileData<T> tile, OutputStream stream) throws IOException { if (tile instanceof SparseTileData<?>) { serializeSparse((SparseTileData<T>) tile, stream); } else { serializeDense(tile, stream); } } private void serializeSparse (SparseTileData<T> tile, OutputStream stream) throws IOException { Schema recordSchema = getRecordSchema(); Schema tileSchema = getTileSchema(StorageType.Sparse); Schema binSchema = tileSchema.getField("values").schema().getElementType(); List<GenericRecord> bins = new ArrayList<GenericRecord>(); Iterator<Pair<BinIndex, T>> i = tile.getData(); while (i.hasNext()) { Pair<BinIndex, T> next = i.next(); BinIndex index = next.getFirst(); T value = next.getSecond(); GenericRecord valueRecord = new GenericData.Record(recordSchema); setValue(valueRecord, value); GenericRecord binRecord = new GenericData.Record(binSchema); binRecord.put("xIndex", index.getX()); binRecord.put("yIndex", index.getY()); binRecord.put("value", valueRecord); bins.add(binRecord); } GenericRecord tileRecord = new GenericData.Record(tileSchema); TileIndex idx = tile.getDefinition(); tileRecord.put("level", idx.getLevel()); tileRecord.put("xIndex", idx.getX()); tileRecord.put("yIndex", idx.getY()); tileRecord.put("xBinCount", idx.getXBins()); tileRecord.put("yBinCount", idx.getYBins()); tileRecord.put("values", bins); tileRecord.put("meta", getTileMetaData(tile)); GenericRecord defaultValueRecord = new GenericData.Record(recordSchema); setValue(defaultValueRecord, tile.getDefaultValue()); tileRecord.put("default", defaultValueRecord); writeRecord(tileRecord, tileSchema, stream); } private void serializeDense (TileData<T> tile, OutputStream stream) throws IOException { Schema recordSchema = getRecordSchema(); Schema tileSchema = getTileSchema(StorageType.Dense); TileIndex idx = tile.getDefinition(); List<GenericRecord> bins = new ArrayList<GenericRecord>(); List<T> denseData = DenseTileData.getData(tile); for (T value: denseData) { GenericRecord bin = new GenericData.Record(recordSchema); setValue(bin, value); bins.add(bin); } GenericRecord tileRecord = new GenericData.Record(tileSchema); tileRecord.put("level", idx.getLevel()); tileRecord.put("xIndex", idx.getX()); tileRecord.put("yIndex", idx.getY()); tileRecord.put("xBinCount", idx.getXBins()); tileRecord.put("yBinCount", idx.getYBins()); tileRecord.put("values", bins); tileRecord.put("meta", getTileMetaData(tile)); T defaultValue = tile.getDefaultValue(); if (null == defaultValue) { tileRecord.put("default", null); } else { GenericRecord defaultValueRecord = new GenericData.Record(recordSchema); setValue(defaultValueRecord, tile.getDefaultValue()); tileRecord.put("default", defaultValueRecord); } writeRecord(tileRecord, tileSchema, stream); } private void writeRecord (GenericRecord record, Schema schema, OutputStream stream) throws IOException { DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<GenericRecord>(schema); DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<GenericRecord>(datumWriter); try { dataFileWriter.setCodec(descriptionToCodec(_compressionCodec)); dataFileWriter.create(schema, stream); dataFileWriter.append(record); dataFileWriter.close(); stream.close(); } catch (IOException e) {throw new RuntimeException("Error serializing",e);} } }