blob: aad0aaf43a9b030c839202f6dbaec56e1b1f9997 [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of serialization;
// TODO(alanknight): We should have an example and tests for subclassing
// serialization rule rather than using the hard-coded ClosureToMap rule. And
// possibly an abstract superclass that's designed to be subclassed that way.
/**
* The abstract superclass for serialization rules.
*/
abstract class SerializationRule {
/**
* Rules belong uniquely to a particular Serialization instance, and can
* be identified within it by number.
*/
int _number;
/**
* Rules belong uniquely to a particular Serialization instance, and can
* be identified within it by number.
*/
int get number => _number;
/**
* Rules belong uniquely to a particular Serialization instance, and can
* be identified within it by number.
*/
void set number(int value) {
if (_number != null && _number != value) throw
new SerializationException("Rule numbers cannot be changed, once set");
_number = value;
}
/**
* Return true if this rule applies to this object, in the context
* where we're writing it, false otherwise.
*/
bool appliesTo(object, Writer writer);
/**
* This extracts the state from the object, calling [f] for each value
* as it is extracted, and returning an object representing the whole
* state at the end. The state that results will still have direct
* pointers to objects, rather than references.
*/
extractState(object, void f(value), Writer w);
/**
* Allows rules to tell us how they expect to store their state. If this
* isn't specified we can also just look at the data to tell.
*/
bool get storesStateAsLists => false;
bool get storesStateAsMaps => false;
bool get storesStateAsPrimitives => false;
/**
* Given the variables representing the state of an object, flatten it
* by turning object pointers into Reference objects where needed. This
* destructively modifies the state object.
*
* This has a default implementation which assumes that object is indexable,
* so either conforms to Map or List. Subclasses may override to do something
* different, including returning a new state object to be used in place
* of the original.
*/
// This has to be a separate operation from extracting, because we extract
// as we are traversing the objects, so we don't yet have the objects to
// generate references for them. It might be possible to avoid that by
// doing a depth-first rather than breadth-first traversal, but I'm not
// sure it's worth it.
flatten(state, Writer writer) {
keysAndValues(state).forEach((key, value) {
var reference = writer._referenceFor(value);
state[key] = reference;
});
}
/** Return true if this rule should only be applied when we are the first
* rule found that applies to this object. This may or may not be a hack
* that will disappear once we have better support for multiple rules.
* We want to have multiple different rules that apply to the same object. We
* also want to have multiple different rules that might exclusively apply
* to the same object. So, we want either ListRule or ListRuleEssential, and
* only one of them can be there. But on the other hand, we may want both
* ListRule and BasicRule. So we identify the kinds of rules that can share.
* If mustBePrimary returns true, then this rule will only be chosen if no
* other rule has been found yet. This means that the ordering of rules in
* the serialization is significant, which is unpleasant, but we'll have
* to see how bad it is.
*/
// TODO(alanknight): Reconsider whether this should be handled differently.
bool get mustBePrimary => false;
/**
* Create the new object corresponding to [state] using the rules
* from [reader]. This may involve recursively inflating "essential"
* references in the state, which are those that are required for the
* object's constructor. It is up to the rule what state is considered
* essential.
*/
inflateEssential(state, Reader reader);
/**
* The [object] has already been created. Set any of its non-essential
* variables from the representation in [state]. Where there are references
* to other objects they are resolved in the context of [reader].
*/
inflateNonEssential(state, object, Reader reader);
/**
* If we have [object] as part of our state, should we represent that
* directly, or should we make a reference for it. By default, true.
* This may also delegate to [writer].
*/
bool shouldUseReferenceFor(object, Writer writer) => true;
/**
* Return true if the data this rule returns is variable length, so a
* length needs to be written for it if the format requires that. Return
* false if the results are always the same length.
*/
bool get hasVariableLengthEntries => true;
/**
* If the data is fixed length, return it here. The format may or may not
* make use of this, depending on whether it already has enough information
* to determine the length on its own. If [hasVariableLengthEntries] is true
* this is ignored.
*/
int get dataLength => 0;
}
/**
* This rule handles things that implement List. It will recreate them as
* whatever the default implemenation of List is on the target platform.
*/
class ListRule extends SerializationRule {
bool appliesTo(object, Writer w) => object is List;
bool get storesStateAsLists => true;
List extractState(List list, f, Writer w) {
var result = new List();
for (var each in list) {
result.add(each);
f(each);
}
return result;
}
inflateEssential(List state, Reader r) => new List();
// For a list, we consider all of its state non-essential and add it
// after creation.
inflateNonEssential(List state, List newList, Reader r) {
populateContents(state, newList, r);
}
void populateContents(List state, List newList, Reader r) {
for(var each in state) {
newList.add(r.inflateReference(each));
}
}
bool get hasVariableLengthEntries => true;
}
/**
* This is a subclass of ListRule where all of the list's contents are
* considered essential state. This is needed if an object X contains a List L,
* but it expects L's contents to be fixed when X's constructor is called.
*/
class ListRuleEssential extends ListRule {
/** Create the new List and also inflate all of its contents. */
inflateEssential(List state, Reader r) {
var object = super.inflateEssential(state, r);
populateContents(state, object, r);
return object;
}
/** Does nothing, because all the work has been done in inflateEssential. */
inflateNonEssential(state, newList, reader) {}
bool get mustBePrimary => true;
}
/**
* This rule handles things that implement Map. It will recreate them as
* whatever the default implemenation of Map is on the target platform. If a
* map has string keys it will attempt to retain it as a map for JSON formats,
* otherwise it will store it as a list of references to keys and values.
*/
class MapRule extends SerializationRule {
bool appliesTo(object, Writer w) => object is Map;
bool get storesStateAsMaps => true;
extractState(Map map, f, Writer w) {
// Note that we make a copy here because flattening may be destructive.
var newMap = new Map.from(map);
newMap.forEach((key, value) {
f(key);
f(value);
});
return newMap;
}
/**
* Change the keys and values of [state] into references in [writer].
* If [state] is a map whose keys are all strings then we leave the keys
* as is so that JSON formats will be more readable. If the keys are
* arbitrary then we need to turn them into references and replace the
* state with a new Map whose keys are the references.
*/
flatten(Map state, Writer writer) {
bool keysAreAllStrings = state.keys.every((x) => x is String);
if (keysAreAllStrings && !writer.shouldUseReferencesForPrimitives) {
keysAndValues(state).forEach(
(key, value) => state[key] = writer._referenceFor(value));
return state;
} else {
var newState = [];
keysAndValues(state).forEach((key, value) {
newState.add(writer._referenceFor(key));
newState.add(writer._referenceFor(value));
});
return newState;
}
}
inflateEssential(state, Reader r) => new Map();
// For a map, we consider all of its state non-essential and add it
// after creation.
inflateNonEssential(state, Map newMap, Reader r) {
if (state is List) {
inflateNonEssentialFromList(state, newMap, r);
} else {
inflateNonEssentialFromMap(state, newMap, r);
}
}
void inflateNonEssentialFromList(List state, Map newMap, Reader r) {
var key;
for (var each in state) {
if (key == null) {
key = each;
} else {
newMap[r.inflateReference(key)] = r.inflateReference(each);
key = null;
}
}
}
void inflateNonEssentialFromMap(Map state, Map newMap, Reader r) {
state.forEach((key, value) {
newMap[r.inflateReference(key)] = r.inflateReference(value);
});
}
bool get hasVariableLengthEntries => true;
}
/**
* This rule handles primitive types, defined as those that we can normally
* represent directly in the output format. We hard-code that to mean
* num, String, and bool.
*/
class PrimitiveRule extends SerializationRule {
bool appliesTo(object, Writer w) => isPrimitive(object);
extractState(object, Function f, Writer w) => object;
flatten(object, Writer writer) {}
inflateEssential(state, Reader r) => state;
inflateNonEssential(object, _, Reader r) {}
bool get storesStateAsPrimitives => true;
/**
* Indicate whether we should save pointers to this object as references
* or store the object directly. For primitives this depends on the format,
* so we delegate to the writer.
*/
bool shouldUseReferenceFor(object, Writer w) =>
w.shouldUseReferencesForPrimitives;
bool get hasVariableLengthEntries => false;
}
/** Typedef for the object construction closure used in ClosureRule. */
typedef ConstructType(Map m);
/** Typedef for the state-getting closure used in ClosureToMapRule. */
typedef Map<String, dynamic> GetStateType(object);
/** Typedef for the state-setting closure used in ClosureToMapRule. */
typedef void NonEssentialStateType(object, Map m);
/**
* This is a rule where the extraction and creation are hard-coded as
* closures. The result is expected to be a map indexed by field name.
*/
class ClosureRule extends CustomRule {
/** The runtimeType of objects that this rule applies to. Used in appliesTo.*/
final Type type;
/** The function for constructing new objects when reading. */
ConstructType construct;
/** The function for returning an object's state as a Map. */
GetStateType getStateFunction;
/** The function for setting an object's state from a Map. */
NonEssentialStateType setNonEssentialState;
/**
* Create a ClosureToMapRule for the given [type] which gets an object's
* state by calling [getState], creates a new object by calling [construct]
* and sets the new object's state by calling [setNonEssentialState].
*/
ClosureRule(this.type, this.getStateFunction, this.construct,
this.setNonEssentialState);
bool appliesTo(object, Writer w) => object.runtimeType == type;
getState(object) => getStateFunction(object);
create(state) => construct(state);
void setState(object, state) {
if (setNonEssentialState == null) return;
setNonEssentialState(object, state);
}
}
/**
* This rule handles things we can't pass directly, but only by reference.
* If objects are listed in the namedObjects in the writer or serialization,
* it will save the name rather than saving the state.
*/
class NamedObjectRule extends SerializationRule {
/**
* Return true if this rule applies to the object. Checked by looking up
* in the namedObjects collection.
*/
bool appliesTo(object, Writer writer) {
return writer.hasNameFor(object);
}
/** Extract the state of the named objects as just the object itself. */
extractState(object, Function f, Writer writer) {
var result = [nameFor(object, writer)];
f(result.first);
return result;
}
/** When we flatten the state we save it as the name. */
// TODO(alanknight): This seems questionable. In a truly flat format we may
// want to have extracted the name as a string first and flatten it into a
// reference to that. But that requires adding the Writer as a parameter to
// extractState, and I'm reluctant to add yet another parameter until
// proven necessary.
flatten(state, Writer writer) {
state[0] = writer._referenceFor(state[0]);
}
/** Look up the named object and return it. */
inflateEssential(state, Reader r) =>
r.objectNamed(r.resolveReference(state.first));
/** Set any non-essential state on the object. For this rule, a no-op. */
inflateNonEssential(state, object, Reader r) {}
/** Return the name for this object in the Writer. */
String nameFor(object, Writer writer) => writer.nameFor(object);
}
/**
* This rule handles the special case of Mirrors. It stores the mirror by its
* qualifiedName and attempts to look it up in both the namedObjects
* collection, or if it's not found there, by looking it up in the mirror
* system. When reading, the user is responsible for supplying the appropriate
* values in [Serialization.namedObjects] or in the [externals] paramter to
* [Serialization.read].
*/
class MirrorRule extends NamedObjectRule {
bool appliesTo(object, Writer writer) => object is DeclarationMirror;
String nameFor(DeclarationMirror object, Writer writer) =>
MirrorSystem.getName(object.qualifiedName);
inflateEssential(state, Reader r) {
var qualifiedName = r.resolveReference(state.first);
var lookupFull = r.objectNamed(qualifiedName, (x) => null);
if (lookupFull != null) return lookupFull;
var separatorIndex = qualifiedName.lastIndexOf(".");
var type = qualifiedName.substring(separatorIndex + 1);
var lookup = r.objectNamed(type, (x) => null);
if (lookup != null) return lookup;
var name = qualifiedName.substring(0, separatorIndex);
// This is very ugly. The library name for an unnamed library is its URI.
// That can't be constructed as a Symbol, so we can't use findLibrary.
// So follow one or the other path depending if it has a colon, which we
// assume is in any URI and can't be in a Symbol.
if (name.contains(":")) {
var uri = Uri.parse(name);
var libMirror = currentMirrorSystem().libraries[uri];
return libMirror.classes[new Symbol(type)];
} else {
var symbol = new Symbol(name);
var typeSymbol = new Symbol(type);
var libMirror = currentMirrorSystem().findLibrary(symbol).firstWhere(
(lib) => lib.classes[typeSymbol] != null);
return libMirror.classes[typeSymbol];
}
}
}
/**
* This provides an abstract superclass for writing your own rules specific to
* a class. It makes some assumptions about behaviour, and so can have a
* simpler set of methods that need to be implemented in order to subclass it.
*
*/
abstract class CustomRule extends SerializationRule {
// TODO(alanknight): It would be nice if we could provide an implementation
// of appliesTo() here. If we add a type parameter to these classes
// we can "is" test against it, but we need to be able to rule out subclasses.
// => instance.runtimeType == T
// should work.
/**
* Return true if this rule applies to this object, in the context
* where we're writing it, false otherwise.
*/
bool appliesTo(instance, Writer w);
/**
* Subclasses should implement this to return a list of the important fields
* in the object. The order of the fields doesn't matter, except that the
* create and setState methods need to know how to use it.
*/
List getState(instance);
/**
* Given a [List] of the object's [state], re-create the object. This should
* do the minimum needed to create the object, just calling the constructor.
* Setting the remaining state of the object should be done in the [setState]
* method, which will be called only once all the objects are created, so
* it won't cause problems with cycles.
*/
create(List state);
/**
* Set any state in [object] which wasn't set in the constructor. Between
* this method and [create] all of the information in [state] should be set
* in the new object.
*/
void setState(object, List state);
extractState(instance, Function f, Writer w) {
var state = getState(instance);
for (var each in values(state)) {
f(each);
}
return state;
}
inflateEssential(state, Reader r) => create(_lazy(state, r));
void inflateNonEssential(state, object, Reader r) {
setState(object, _lazy(state, r));
}
// We don't want to have to make the end user tell us how long the list is
// separately, so write it out for each object, even though they're all
// expected to be the same length.
bool get hasVariableLengthEntries => true;
}
/** A hard-coded rule for serializing Symbols. */
class SymbolRule extends CustomRule {
bool appliesTo(instance, _) => instance is Symbol;
getState(instance) => [MirrorSystem.getName(instance)];
create(state) => new Symbol(state[0]);
void setState(symbol, state) {}
int get dataLength => 1;
bool get hasVariableLengthEntries => false;
}
/** A hard-coded rule for DateTime. */
class DateTimeRule extends CustomRule {
bool appliesTo(instance, _) => instance is DateTime;
List getState(DateTime date) => [date.millisecondsSinceEpoch, date.isUtc];
create(List state) =>
new DateTime.fromMillisecondsSinceEpoch(state[0], isUtc: state[1]);
void setState(date, state) {}
// Let the system know we don't have to store a length for these.
int get dataLength => 2;
bool get hasVariableLengthEntries => false;
}
/** Create a lazy list/map that will inflate its items on demand in [r]. */
_lazy(l, Reader r) {
if (l is List) return new _LazyList(l, r);
if (l is Map) return new _LazyMap(l, r);
throw new SerializationException("Invalid type: must be Map or List - $l");
}
/**
* This provides an implementation of Map that wraps a list which may
* contain references to (potentially) non-inflated objects. If these
* are accessed it will inflate them. This allows us to pass something that
* looks like it's just a list of objects to a [CustomRule] without needing
* to inflate all the references in advance.
*/
class _LazyMap implements Map {
_LazyMap(this._raw, this._reader);
final Map _raw;
final Reader _reader;
// This is the only operation that really matters.
operator [](x) => _reader.inflateReference(_raw[x]);
int get length => _raw.length;
bool get isEmpty => _raw.isEmpty;
bool get isNotEmpty => _raw.isNotEmpty;
Iterable get keys => _raw.keys;
bool containsKey(x) => _raw.containsKey(x);
// These operations will work, but may be expensive, and are probably
// best avoided.
get _inflated => keysAndValues(_raw).map(_reader.inflateReference);
bool containsValue(x) => _inflated.containsValue(x);
Iterable get values => _inflated.values;
void forEach(f) => _inflated.forEach(f);
// These operations are all invalid
_throw() {
throw new UnsupportedError("Not modifiable");
}
operator []=(x, y) => _throw();
putIfAbsent(x, y) => _throw();
remove(x) => _throw();
clear() => _throw();
}
/**
* This provides an implementation of List that wraps a list which may
* contain references to (potentially) non-inflated objects. If these
* are accessed it will inflate them. This allows us to pass something that
* looks like it's just a list of objects to a [CustomRule] without needing
* to inflate all the references in advance.
*/
class _LazyList extends IterableBase implements List {
_LazyList(this._raw, this._reader);
final List _raw;
final Reader _reader;
// This is the only operation that really matters.
operator [](x) => _reader.inflateReference(_raw[x]);
int get length => _raw.length;
bool get isEmpty => _raw.isEmpty;
get first => _reader.inflateReference(_raw.first);
get last => _reader.inflateReference(_raw.last);
// These operations, and other inherited methods that iterate over the whole
// list will work, but may be expensive, and are probably
// best avoided.
Iterable get _inflated => _raw.map(_reader.inflateReference);
Iterator get iterator => _inflated.iterator;
indexOf(x, [pos = 0]) => _inflated.toList().indexOf(x);
lastIndexOf(x, [pos]) => _inflated.toList().lastIndexOf(x);
sublist(start, [end]) => _inflated.sublist(start, end);
Map<int, dynamic> asMap() => _inflated.asMap();
// These operations are all invalid
_throw() {
throw new UnsupportedError("Not modifiable");
}
operator []=(x, y) => _throw();
add(x) => _throw();
addAll(x) => _throw();
sort([f]) => _throw();
clear() => _throw();
insert(x, y) => _throw();
insertAll(x, y) => _throw();
fillRange(x, y, [z]) => _throw();
removeAt(x) => _throw();
remove(x) => _throw();
removeLast() => _throw();
removeAll(x) => _throw();
retainAll(x) => _throw();
removeWhere(x) => _throw();
retainWhere(x) => _throw();
replaceRange(x, y, z) => _throw();
getRange(x, y) => _throw();
setRange(x, y, z, [a = 0]) => _throw();
setAll(x, y) => _throw();
removeRange(x, y) => _throw();
get reversed => _throw();
void set length(x) => _throw();
}