/* * Copyright (C) 2012 Facebook, Inc. * * 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.facebook.data.types; import com.facebook.collectionsbase.Lists; import com.facebook.util.serialization.SerDe; import com.facebook.util.serialization.SerDeException; import org.json.JSONException; import org.json.JSONObject; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Datum is a very generic class that encapsulates actual data as well * as methods to inspect what it is. */ public class MapDatum implements Datum { private static final Comparator<Map.Entry<Datum, Datum>> ENTRY_COMPARATOR = new Comparator<Map.Entry<Datum, Datum>>() { @Override public int compare( Map.Entry<Datum, Datum> o1, Map.Entry<Datum, Datum> o2 ) { int keyResult = o1.getKey().compareTo(o2.getKey()); if (keyResult == 0) { return o1.getValue().compareTo(o2.getValue()); } return keyResult; } }; private final static DatumSerDe DATUM_SER_DE = new DatumSerDe(); private final Map<Datum, Datum> map; public MapDatum(Map<Datum, Datum> map) { this.map = map; } public MapDatum() { this(new HashMap<Datum, Datum>()); } /** * @return true if the hash is non-empty */ @Override public boolean asBoolean() { return !map.isEmpty(); } /** * number of keys; may overflow * * @return */ @Override public byte asByte() { return (byte) map.size(); } /** * number of keys; may overflow * * @return */ @Override public short asShort() { return (short) map.size(); } /** * number of keys * * @return */ @Override public int asInteger() { return map.size(); } /** * number of keys * * @return */ @Override public long asLong() { return map.size(); } @Override public float asFloat() { throw new UnsupportedOperationException(); } @Override public double asDouble() { throw new UnsupportedOperationException(); } /** * nested data structure are rendered with asString() * * @return JSON representation of map { k1 : v1, k2 : v2, ...} */ @Override public String asString() { JSONObject jsonObject = new JSONObject(); try { for (Map.Entry<Datum, Datum> entry : map.entrySet()) { String key = entry.getKey().asString(); String value = entry.getValue().asString(); jsonObject.put(key, value); } return jsonObject.toString(2); } catch (JSONException e) { throw new RuntimeException("error converting json object to string"); } } @Override public byte[] asBytes() { try { // todo: use jackson to to JSON encoding, or can we somehow just // call asBytes() on each key/value and concatenate? // (also, as this is used for unique counts, who does a unique // on Map? watch a use cometh...) return asString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("failed to encode as UTF-8"); } } @Override public boolean isNull() { return false; } @Override public DatumType getType() { return DatumType.MAP; } @Override public Object asRaw() { return map; } public Map<Datum, Datum> getMap() { return map; } @Override public String toString() { return asString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof MapDatum)) { return false; } final MapDatum mapDatum = (MapDatum) o; if (map != null ? !map.equals(mapDatum.map) : mapDatum.map != null) { return false; } return true; } @Override public int hashCode() { return map != null ? map.hashCode() : 0; } @Override public int compareTo(Datum o) { if (!(o instanceof MapDatum)) { throw new IllegalArgumentException("need MapDatum"); } @SuppressWarnings({"unchecked"}) MapDatum otherMapDatum = (MapDatum) o; // this is only used in InMemoryStorage for unit tests, so it can e // inefficient; this is terribly inefficient, but it provides // some way to comapre maps :) List<Map.Entry<Datum, Datum>> entryList1 = new ArrayList<>(map.entrySet()); List<Map.Entry<Datum, Datum>> entryList2 = new ArrayList<>(otherMapDatum.map.entrySet()); Collections.sort(entryList1, ENTRY_COMPARATOR); Collections.sort(entryList2, ENTRY_COMPARATOR); return Lists.compareLists(entryList1, entryList2, ENTRY_COMPARATOR); } public static class SerDeImpl implements SerDe<Datum> { @Override public Datum deserialize(DataInput in) throws SerDeException { try { int numEntires = in.readInt(); Map<Datum, Datum> map = new HashMap<>(numEntires); for (int i = 0; i < numEntires; i++) { Datum key = DATUM_SER_DE.deserialize(in); Datum value = DATUM_SER_DE.deserialize(in); map.put(key, value); } MapDatum result = new MapDatum(map); return result; } catch (IOException e) { throw new SerDeException(e); } } @Override public void serialize(Datum value, DataOutput out) throws SerDeException { if (value instanceof MapDatum) { MapDatum mapDatum = (MapDatum) value; Map<Datum, Datum> map = mapDatum.getMap(); try { out.writeInt(map.size()); for (Map.Entry<Datum, Datum> entry : map.entrySet()) { DATUM_SER_DE.serialize(entry.getKey(), out); DATUM_SER_DE.serialize(entry.getValue(), out); } } catch (IOException e) { throw new SerDeException(e); } } else { throw new IllegalArgumentException( "MapDatum.SerDe serializer requires MapDatum" ); } } } }