Skip to content

Commit

Permalink
Add toJSON serialisation option to deduplicate resources in graph (hy…
Browse files Browse the repository at this point in the history
…perledger-archives#1673)

* added serialization option to support Jackson, updated Java codegen, allow serialization options to be passed for HTTP post

* Updated changelog after merge

* updated system test

* Added annotation on concepts to ignore

* removed dead comment
  • Loading branch information
dselman authored and Simon Stone committed Jul 26, 2017
1 parent 3627081 commit 1855c58
Show file tree
Hide file tree
Showing 16 changed files with 692 additions and 70 deletions.
2 changes: 1 addition & 1 deletion packages/composer-common/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class SecurityException extends BaseException {
}
class Serializer {
+ void constructor(Factory,ModelManager)
+ Object toJSON(Resource,Object,boolean,boolean,boolean) throws Error
+ Object toJSON(Resource,Object,boolean,boolean,boolean,boolean) throws Error
+ Resource fromJSON(Object,Object,boolean)
}
class ValidationException extends BaseException {
Expand Down
3 changes: 2 additions & 1 deletion packages/composer-common/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
#
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#
Version 0.10.1 {5d850eefdff10604eb0fdef2bd7aef5a} 2017-07-24
Version 0.10.1 {d1fd512551ff5bb30b31f05f6817966e} 2017-07-24
- Added InvalidQueryException, BaseFileException
- Added IdCard to composer-common package
- Added option to Serializer.toJSON to deduplicate resources

Version 0.9.2 {60327250ee4f059647020f8aee5ed67b} 2017-07-06
- Added includeOptionalFields option to Factory.newXXX functions
Expand Down
60 changes: 53 additions & 7 deletions packages/composer-common/lib/codegen/fromcto/java/javavisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,33 @@ class JavaVisitor {
* @private
*/
visitModelManager(modelManager, parameters) {

parameters.fileWriter.openFile( 'org/hyperledger/composer/system/Resource.java');
parameters.fileWriter.writeLine(0, '// this code is generated and should not be modified');
parameters.fileWriter.writeLine(0, 'package org.hyperledger.composer.system;');
parameters.fileWriter.writeLine(0, 'import com.fasterxml.jackson.annotation.*;');

parameters.fileWriter.writeLine(0, `
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "$class")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "$id")
public abstract class Resource
{
public abstract String getID();
private String $id;
@JsonProperty("$id")
public String get$id() {
return $id;
}
@JsonProperty("$id")
public void set$id(String i) {
$id = i;
}
}
`);
parameters.fileWriter.closeFile();

modelManager.getModelFiles().forEach((modelFile) => {
modelFile.accept(this,parameters);
});
Expand Down Expand Up @@ -122,11 +149,7 @@ class JavaVisitor {
parameters.fileWriter.writeLine(0, '// this code is generated and should not be modified');
parameters.fileWriter.writeLine(0, 'package ' + clazz.getModelFile().getNamespace() + ';');
parameters.fileWriter.writeLine(0, '');

if(!clazz.isSystemType()) {
parameters.fileWriter.writeLine(0, 'import org.hyperledger.composer.system.*;');
parameters.fileWriter.writeLine(0, 'import com.fasterxml.jackson.annotation.*;');
}
parameters.fileWriter.writeLine(0, 'import org.hyperledger.composer.system.*;');
}

/**
Expand Down Expand Up @@ -177,6 +200,16 @@ class JavaVisitor {

this.startClassFile(classDeclaration, parameters);

classDeclaration.getModelFile().getImports().forEach((imported) => {
parameters.fileWriter.writeLine(0, 'import ' + imported + ';' );
});

if(classDeclaration.isConcept()) {
parameters.fileWriter.writeLine(0, 'import com.fasterxml.jackson.annotation.JsonIgnoreProperties;');
parameters.fileWriter.writeLine(0, '');
parameters.fileWriter.writeLine(0, '@JsonIgnoreProperties({"$class"})');
}

let isAbstract = '';
if( classDeclaration.isAbstract() ) {
isAbstract = 'abstract ';
Expand All @@ -186,14 +219,27 @@ class JavaVisitor {
}

let superType = '';

if(classDeclaration.isSystemCoreType()) {
superType = ' extends org.hyperledger.composer.system.Resource';
}

if(classDeclaration.getSuperType()) {
superType = ' extends ' + ModelUtil.getShortName(classDeclaration.getSuperType());
}

parameters.fileWriter.writeLine(0, 'public ' + isAbstract + 'class ' + classDeclaration.getName() + superType + ' {' );

// add the magic $class property
parameters.fileWriter.writeLine(1, 'public String $class;' );
// add the getID abstract type
if(classDeclaration.getIdentifierFieldName()) {
parameters.fileWriter.writeLine(1, `
// the accessor for the identifying field
public String getID() {
return ${classDeclaration.getIdentifierFieldName()};
}
`
);
}

classDeclaration.getOwnProperties().forEach((property) => {
property.accept(this,parameters);
Expand Down
1 change: 0 additions & 1 deletion packages/composer-common/lib/codegen/jsonwriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class JSONWriter extends Writer {
* @param {string} value - the value
*/
writeKeyStringValue(key,value) {
this.writeComma();
this.writeKey(key);
this.writeStringValue(value);
this.firstItem = false;
Expand Down
7 changes: 6 additions & 1 deletion packages/composer-common/lib/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class Serializer {
* are specified for relationship fields into relationships, false by default.
* @param {boolean} options.permitResourcesForRelationships - Permit resources in the
* place of relationships (serializing them as resources), false by default.
* @param {boolean} options.deduplicateResources - Generate $id for resources and
* if a resources appears multiple times in the object graph only the first instance is
* serialized in full, subsequent instances are replaced with a reference to the $id
* @return {Object} - The Javascript Object that represents the resource
* @throws {Error} - throws an exception if resource is not an instance of
* Resource or fails validation.
Expand All @@ -79,6 +82,7 @@ class Serializer {
parameters.stack = new TypedStack(resource);
parameters.modelManager = this.modelManager;
parameters.seenResources = new Set();
parameters.dedupeResources = new Set();
const classDeclaration = this.modelManager.getType( resource.getFullyQualifiedType() );

// validate the resource against the model
Expand All @@ -93,7 +97,8 @@ class Serializer {

const generator = new JSONGenerator(
options.convertResourcesToRelationships === true,
options.permitResourcesForRelationships === true
options.permitResourcesForRelationships === true,
options.deduplicateResources === true
);
const writer = new JSONWriter();
parameters.writer = writer;
Expand Down
129 changes: 78 additions & 51 deletions packages/composer-common/lib/serializer/jsongenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ClassDeclaration = require('../introspect/classdeclaration');
const Field = require('../introspect/field');
const RelationshipDeclaration = require('../introspect/relationshipdeclaration');
const Resource = require('../model/resource');
const Identifiable = require('../model/identifiable');
const Typed = require('../model/typed');
const Concept = require('../model/concept');
const ModelUtil = require('../modelutil');
Expand All @@ -42,10 +43,14 @@ class JSONGenerator {
* are specified for relationship fields into relationships, false by default.
* @param {boolean} [permitResourcesForRelationships] Permit resources in the
* place of relationships (serializing them as resources), false by default.
* @param {boolean} [deduplicateResources] If resources appear several times
* in the object graph only the first instance is serialized, with only the $id
* written for subsequent instances, false by default.
*/
constructor(convertResourcesToRelationships, permitResourcesForRelationships) {
constructor(convertResourcesToRelationships, permitResourcesForRelationships, deduplicateResources) {
this.convertResourcesToRelationships = convertResourcesToRelationships;
this.permitResourcesForRelationships = permitResourcesForRelationships;
this.deduplicateResources = deduplicateResources;
}

/**
Expand All @@ -63,7 +68,7 @@ class JSONGenerator {
} else if (thing instanceof Field) {
return this.visitField(thing, parameters);
} else {
throw new Error('Unrecognised ' + JSON.stringify(thing) );
throw new Error('Unrecognised ' + JSON.stringify(thing));
}
}

Expand All @@ -75,24 +80,47 @@ class JSONGenerator {
* @private
*/
visitClassDeclaration(classDeclaration, parameters) {
parameters.writer.openObject();
parameters.writer.writeKeyStringValue('$class', classDeclaration.getFullyQualifiedName());

const obj = parameters.stack.pop();
if(!((obj instanceof Resource) || (obj instanceof Concept))) {
throw new Error('Expected a Resource or a Concept, but found ' + obj );
if (!((obj instanceof Resource) || (obj instanceof Concept))) {
throw new Error('Expected a Resource or a Concept, but found ' + obj);
}
const properties = classDeclaration.getProperties();
for(let n=0; n < properties.length; n++) {
const property = properties[n];
const value = obj[property.getName()];
if(!Util.isNull(value)) {
parameters.stack.push(value);
property.accept(this,parameters);

let writeFields = true;
let id = null;

if (obj instanceof Identifiable && this.deduplicateResources) {
id = obj.toURI();
if( parameters.dedupeResources.has(id)) {
writeFields = false;
parameters.writer.writeStringValue( id );
}
else {
parameters.dedupeResources.add(id);
}
}

if (writeFields) {
parameters.writer.openObject();
parameters.writer.writeKeyStringValue('$class', classDeclaration.getFullyQualifiedName());

if(this.deduplicateResources && id) {
parameters.writer.writeKeyStringValue('$id', id );
}

const properties = classDeclaration.getProperties();
for (let n = 0; n < properties.length; n++) {
const property = properties[n];
const value = obj[property.getName()];
if (!Util.isNull(value)) {
parameters.stack.push(value);
property.accept(this, parameters);
}
}

parameters.writer.closeObject();
}

parameters.writer.closeObject();
return null;
}

Expand All @@ -106,26 +134,23 @@ class JSONGenerator {
visitField(field, parameters) {
const obj = parameters.stack.pop();
parameters.writer.writeKey(field.getName());
if(field.isArray()) {
if (field.isArray()) {
parameters.writer.openArray();
for(let n=0; n < obj.length; n++) {
for (let n = 0; n < obj.length; n++) {
const item = obj[n];
if(!field.isPrimitive() && !ModelUtil.isEnum(field)) {
if (!field.isPrimitive() && !ModelUtil.isEnum(field)) {
parameters.writer.writeComma();
parameters.stack.push(item, Typed);
const classDecl = parameters.modelManager.getType(item.getFullyQualifiedType());
classDecl.accept(this, parameters);
}
else {
parameters.writer.writeArrayValue(this.convertToJSON(field,item));
} else {
parameters.writer.writeArrayValue(this.convertToJSON(field, item));
}
}
parameters.writer.closeArray();
}
else if(field.isPrimitive() || ModelUtil.isEnum(field)) {
parameters.writer.writeValue(this.convertToJSON(field,obj));
}
else {
} else if (field.isPrimitive() || ModelUtil.isEnum(field)) {
parameters.writer.writeValue(this.convertToJSON(field, obj));
} else {
parameters.stack.push(obj);
const classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType());
classDeclaration.accept(this, parameters);
Expand All @@ -142,19 +167,22 @@ class JSONGenerator {
* @return {string} the text representation
*/
convertToJSON(field, obj) {
switch(field.getType()) {
case 'DateTime': {
return `"${obj.toISOString()}"`;
}
switch (field.getType()) {
case 'DateTime':
{
return `"${obj.toISOString()}"`;
}
case 'Integer':
case 'Long':
case 'Double':
case 'Boolean':{
return `${obj.toString()}`;
}
default: {
return JSON.stringify(obj.toString());
}
case 'Boolean':
{
return `${obj.toString()}`;
}
default:
{
return JSON.stringify(obj.toString());
}
}
}

Expand All @@ -169,14 +197,14 @@ class JSONGenerator {
const obj = parameters.stack.pop();
parameters.writer.writeKey(relationshipDeclaration.getName());

if(relationshipDeclaration.isArray()) {
if (relationshipDeclaration.isArray()) {
parameters.writer.openArray();
for(let n=0; n < obj.length; n++) {
for (let n = 0; n < obj.length; n++) {
const item = obj[n];
if (this.permitResourcesForRelationships && item instanceof Resource) {
let fqi = item.getFullyQualifiedIdentifier();
if (parameters.seenResources.has(fqi)) {
let relationshipText = this.getRelationshipText(relationshipDeclaration, item );
let relationshipText = this.getRelationshipText(relationshipDeclaration, item);
parameters.writer.writeStringValue(relationshipText);
} else {
parameters.seenResources.add(fqi);
Expand All @@ -187,16 +215,15 @@ class JSONGenerator {
parameters.seenResources.delete(fqi);
}
} else {
let relationshipText = this.getRelationshipText(relationshipDeclaration, item );
let relationshipText = this.getRelationshipText(relationshipDeclaration, item);
parameters.writer.writeArrayStringValue(relationshipText);
}
}
parameters.writer.closeArray();
}
else if (this.permitResourcesForRelationships && obj instanceof Resource) {
} else if (this.permitResourcesForRelationships && obj instanceof Resource) {
let fqi = obj.getFullyQualifiedIdentifier();
if (parameters.seenResources.has(fqi)) {
let relationshipText = this.getRelationshipText(relationshipDeclaration, obj );
let relationshipText = this.getRelationshipText(relationshipDeclaration, obj);
parameters.writer.writeStringValue(relationshipText);
} else {
parameters.seenResources.add(fqi);
Expand All @@ -206,24 +233,24 @@ class JSONGenerator {
parameters.seenResources.delete(fqi);
}
} else {
let relationshipText = this.getRelationshipText(relationshipDeclaration, obj );
let relationshipText = this.getRelationshipText(relationshipDeclaration, obj);
parameters.writer.writeStringValue(relationshipText);
}
return null;
}

/**
* Returns the persistent format for a relationship.
* @param {RelationshipDeclaration} relationshipDeclaration - the relationship being persisted
* @param {Identifiable} relationshipOrResource - the relationship or the resource
* @returns {string} the text to use to persist the relationship
*/
* Returns the persistent format for a relationship.
* @param {RelationshipDeclaration} relationshipDeclaration - the relationship being persisted
* @param {Identifiable} relationshipOrResource - the relationship or the resource
* @returns {string} the text to use to persist the relationship
*/
getRelationshipText(relationshipDeclaration, relationshipOrResource) {
if(relationshipOrResource instanceof Resource) {
if (relationshipOrResource instanceof Resource) {
const allowRelationships =
this.convertResourcesToRelationships || this.permitResourcesForRelationships;
if(!allowRelationships) {
throw new Error('Did not find a relationship for ' + relationshipDeclaration.getFullyQualifiedTypeName() + ' found ' + relationshipOrResource );
if (!allowRelationships) {
throw new Error('Did not find a relationship for ' + relationshipDeclaration.getFullyQualifiedTypeName() + ' found ' + relationshipOrResource);
}
}
return relationshipOrResource.toURI();
Expand Down
Loading

0 comments on commit 1855c58

Please sign in to comment.