io.protostuff.GraphProtostuffOutput Maven / Gradle / Ivy
Show all versions of protostuff-core Show documentation
//========================================================================
//Copyright 2007-2011 David Yu [email protected]
//------------------------------------------------------------------------
//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 io.protostuff;
import static io.protostuff.WireFormat.WIRETYPE_END_GROUP;
import static io.protostuff.WireFormat.WIRETYPE_REFERENCE;
import static io.protostuff.WireFormat.WIRETYPE_START_GROUP;
import static io.protostuff.WireFormat.makeTag;
import java.io.IOException;
import java.util.Map;
/**
* A ProtostuffOutput w/c can handle cyclic dependencies when serializing objects with graph transformations.
*
* @author David Yu
* @created Dec 10, 2010
*/
public final class GraphProtostuffOutput extends FilterOutput
{
private final IdentityMap references;
private int refCount = 0;
public GraphProtostuffOutput(ProtostuffOutput output)
{
super(output);
references = new IdentityMap();
}
public GraphProtostuffOutput(ProtostuffOutput output, int initialCapacity)
{
super(output);
references = new IdentityMap(initialCapacity);
}
@Override
public void writeObject(int fieldNumber, T value, Schema schema,
boolean repeated) throws IOException
{
final ProtostuffOutput output = this.output;
if (references.shouldIncrement(refCount, value, output, fieldNumber))
{
refCount++;
output.tail = output.sink.writeVarInt32(
makeTag(fieldNumber, WIRETYPE_START_GROUP),
output,
output.tail);
schema.writeTo(this, value);
output.tail = output.sink.writeVarInt32(
makeTag(fieldNumber, WIRETYPE_END_GROUP),
output,
output.tail);
}
}
/**
* A trimed-down version of IdentityHashMap w/c caters to the specific needs of {@link GraphOutput}.
*/
private static final class IdentityMap
{
/**
* The initial capacity used by the no-args constructor. MUST be a power of two. The value 32 corresponds to the
* (specified) expected maximum size of 21, given a load factor of 2/3.
*/
private static final int DEFAULT_CAPACITY = 32;
/**
* The minimum capacity, used if a lower value is implicitly specified by either of the constructors with
* arguments. The value 4 corresponds to an expected maximum size of 2, given a load factor of 2/3. MUST be a
* power of two.
*/
private static final int MINIMUM_CAPACITY = 4;
/**
* The maximum capacity, used if a higher value is implicitly specified by either of the constructors with
* arguments. MUST be a power of two <= 1<<29.
*/
private static final int MAXIMUM_CAPACITY = 1 << 29;
/**
* The table, resized as necessary. Length MUST always be a power of two.
*/
private transient Object[] table;
/**
* The number of key-value mappings contained in this identity hash map.
*
* @serial
*/
private int size;
/*
* The number of modifications, to support fast-fail iterators
*/
// private transient volatile int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*/
private transient int threshold;
/**
* Constructs a new, empty identity hash map with a default expected maximum size (21).
*/
public IdentityMap()
{
init(DEFAULT_CAPACITY);
}
/**
* Constructs a new, empty map with the specified expected maximum size. Putting more than the expected number
* of key-value mappings into the map may cause the internal data structure to grow, which may be somewhat
* time-consuming.
*
* @param expectedMaxSize
* the expected maximum size of the map
* @throws IllegalArgumentException
* if expectedMaxSize is negative
*/
public IdentityMap(int expectedMaxSize)
{
if (expectedMaxSize < 0)
throw new IllegalArgumentException("expectedMaxSize is negative: " + expectedMaxSize);
init(capacity(expectedMaxSize));
}
/**
* Returns the appropriate capacity for the specified expected maximum size. Returns the smallest power of two
* between MINIMUM_CAPACITY and MAXIMUM_CAPACITY, inclusive, that is greater than (3 * expectedMaxSize)/2, if
* such a number exists. Otherwise returns MAXIMUM_CAPACITY. If (3 * expectedMaxSize)/2 is negative, it is
* assumed that overflow has occurred, and MAXIMUM_CAPACITY is returned.
*/
private int capacity(int expectedMaxSize)
{
// Compute min capacity for expectedMaxSize given a load factor of 2/3
int minCapacity = (3 * expectedMaxSize) / 2;
// Compute the appropriate capacity
int result;
if (minCapacity > MAXIMUM_CAPACITY || minCapacity < 0)
{
result = MAXIMUM_CAPACITY;
}
else
{
result = MINIMUM_CAPACITY;
while (result < minCapacity)
result <<= 1;
}
return result;
}
/**
* Initializes object to be an empty map with the specified initial capacity, which is assumed to be a power of
* two between MINIMUM_CAPACITY and MAXIMUM_CAPACITY inclusive.
*/
private void init(int initCapacity)
{
// assert (initCapacity & -initCapacity) == initCapacity; // power of 2
// assert initCapacity >= MINIMUM_CAPACITY;
// assert initCapacity <= MAXIMUM_CAPACITY;
threshold = (initCapacity * 2) / 3;
table = new Object[2 * initCapacity];
}
/**
* Returns index for Object x.
*/
private static int hash(Object x, int length)
{
int h = System.identityHashCode(x);
// Multiply by -127, and left-shift to use least bit as part of hash
return ((h << 1) - (h << 8)) & (length - 1);
}
/**
* Circularly traverses table of size len.
*/
private static int nextKeyIndex(int i, int len)
{
return (i + 2 < len ? i + 2 : 0);
}
/*
* Returns the value to which the specified key is mapped, or {@code null} if this map contains no mapping for
* the key.
*
* More formally, if this map contains a mapping from a key {@code k} to a value {@code v} such that {@code
* (key == k)}, then this method returns {@code v}; otherwise it returns {@code null}. (There can be at most one
* such mapping.)
*
*
A return value of {@code null} does not necessarily indicate that the map contains no mapping for
* the key; it's also possible that the map explicitly maps the key to {@code null}. The {@link #containsKey
* containsKey} operation may be used to distinguish these two cases.
*
* @see #put(Object, Object)
*
* public Integer get(Object k) { Object[] tab = table; int len = tab.length; int i = hash(k, len); while (true)
* { Object item = tab[i]; if (item == k) return (Integer)tab[i + 1]; if (item == null) return null; i =
* nextKeyIndex(i, len); } }
*
* /* Associates the specified value with the specified key in this identity hash map. If the map previously
* contained a mapping for the key, the old value is replaced.
*
* @param key the key with which the specified value is to be associated
*
* @param value the value to be associated with the specified key
*
* @return the previous value associated with key, or null if there was no mapping for
* key. (A null return can also indicate that the map previously associated null with
* key.)
*
* @see Object#equals(Object)
*
* @see #get(Object)
*
* @see #containsKey(Object)
*
* public Integer put(Object k, Integer value) { Object[] tab = table; int len = tab.length; int i = hash(k,
* len);
*
* Object item; while ((item = tab[i]) != null) { if (item == k) { Integer oldValue = (Integer)tab[i + 1]; tab[i
* + 1] = value; return oldValue; } i = nextKeyIndex(i, len); }
*
* // modCount++; tab[i] = k; tab[i + 1] = value; if (++size >= threshold) resize(len); // len == 2 * current
* capacity. return null; }
*/
/**
* Returns true if the provided int should increment(unique index id).
*/
public boolean shouldIncrement(int value, Object k, WriteSession output,
int fieldNumber) throws IOException
{
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
Object item;
while ((item = tab[i]) != null)
{
if (item == k)
{
if (k instanceof Map.Entry
// filter on standard java map impls only
&& k.getClass().getName().startsWith("java.util"))
{
// IdentityHashMap and EnumMap re-uses the same Map.Entry.
// It simply holds references to the actual data (key/value).
return true;
}
output.tail = output.sink.writeVarInt32(
((Integer) tab[i + 1]).intValue(),
output,
output.sink.writeVarInt32(
makeTag(fieldNumber, WIRETYPE_REFERENCE),
output,
output.tail));
return false;
}
i = nextKeyIndex(i, len);
}
// modCount++;
tab[i] = k;
tab[i + 1] = Integer.valueOf(value);
if (++size >= threshold)
resize(len); // len == 2 * current capacity.
return true;
}
/**
* Resize the table to hold given capacity.
*
* @param newCapacity
* the new capacity, must be a power of two.
*/
private void resize(int newCapacity)
{
// assert (newCapacity & -newCapacity) == newCapacity; // power of 2
int newLength = newCapacity * 2;
Object[] oldTable = table;
int oldLength = oldTable.length;
if (oldLength == 2 * MAXIMUM_CAPACITY)
{ // can't expand any further
if (threshold == MAXIMUM_CAPACITY - 1)
throw new IllegalStateException("Capacity exhausted.");
threshold = MAXIMUM_CAPACITY - 1; // Gigantic map!
return;
}
if (oldLength >= newLength)
return;
Object[] newTable = new Object[newLength];
threshold = newLength / 3;
for (int j = 0; j < oldLength; j += 2)
{
Object key = oldTable[j];
if (key != null)
{
Object value = oldTable[j + 1];
oldTable[j] = null;
oldTable[j + 1] = null;
int i = hash(key, newLength);
while (newTable[i] != null)
i = nextKeyIndex(i, newLength);
newTable[i] = key;
newTable[i + 1] = value;
}
}
table = newTable;
}
}
}