emfjson / emfjson-jackson

JSON Binding for Eclipse Modeling Framework
https://emfjson.github.io
Other
80 stars 23 forks source link

More flexibility for the eClass field order #45

Closed halwax closed 9 years ago

halwax commented 9 years ago

Uppon loading of a json content I can't guarantee the order in which the attributes are listed in the json. This leads to a problem on loading of the resource, because EMFJson skips the attributes and references until it gets the eClass field (in case it doesn't know the correct type in advance).

The following test snippets reproduce this behaviour. Note that you have to register ´EPackage.Registry.INSTANCE.put(EcorePackage.eNS_URI, EcorePackage.eINSTANCE);` in the org.emfjson.jackson.junit.support.TestSupport class to make the snippets work.

@Test
def testLoadWithEClassLast() throws IOException {
    val data ='''
        {
            "name" : "A",
            "eClass" : "http://www.eclipse.org/emf/2002/Ecore#//EClass"
        }
    '''

    val resource = resourceSet.createResource(URI.createURI("tests/test.json"))
    resource.load(new ByteArrayInputStream(data.bytes), options)

    assertFalse(resource.contents.empty)

    val eClass = resource.contents.get(0) as EClass

    assertNotNull(eClass)
    assertEquals(eClass.getName(),"A")
}

This test shows that the name attribute gets lost.

In case of an list this also results in a NullPointer.

@Test
def testLoadWithFeaturesEClassLast() throws IOException {

    val data ='''
        {
            "name" : "A",
            "eStructuralFeatures" : [ {
                "name" : "text",
                "eType" : {
                    "$ref" : "http://www.eclipse.org/emf/2002/Ecore#//EString"
                },
                "eClass" : "http://www.eclipse.org/emf/2002/Ecore#//EAttribute"
            }],
            "eClass" : "http://www.eclipse.org/emf/2002/Ecore#//EClass"
        }
    '''

    val resource = resourceSet.createResource(URI.createURI("tests/test.json"))
    resource.load(new ByteArrayInputStream(data.bytes), options)

    assertFalse(resource.contents.empty)

    val eClass = resource.contents.get(0) as EClass
    eClass.assertFeatures
}

Would it be possible to adapt the StreamReader class to deal with json content with the eClass not garantueed to be the first field?

Thanks and regards, Michael

halwax commented 9 years ago

The following changes in the StreamReader class would resolve my issues (the provided tests are green). Note that since all the needed classes are visible (in terms of osgi exports and java inheritance) I was able to extend JsonResourceFactory, JsonResource and StreamReader and override for my implementation. In case you take this approach into consideration for the emfjson-jackson project I'd be glad to contribute it.

protected EObject parseObject(JsonParser parser, EReference containment,
        EObject owner, EClass currentClass) throws JsonParseException,
        IOException {

    EObject current = null;

    if (currentClass != null) {
        current = EcoreUtil.create(currentClass);
    }

    TokenBuffer tokenBuffer = new TokenBuffer(null, false);

    while (parser.nextToken() != JsonToken.END_OBJECT) {

        final String fieldname = parser.getCurrentName();

        if (EJS_TYPE_KEYWORD.equals(fieldname)) {

            EObject oldCurrent = current;
            current = create(parser.nextTextValue());

            if(oldCurrent!=null) {
                copyOldFeatures(current, oldCurrent);
            }

        } else if (EJS_UUID_ANNOTATION.equals(fieldname)) {

            if (resource instanceof UuidResource) {
                if (current != null) {
                    ((UuidResource) resource).setID(current,
                            parser.nextTextValue());
                }
            }

        } else {

            if (current == null && containment != null) {
            EClass defaultType = containment.getEReferenceType();
                if (!defaultType.isAbstract()) {
                    current = EcoreUtil.create(defaultType);
                }
            }

            if (current != null) {

                boolean knownFeature = readFeature(parser, current, fieldname);
                if(!knownFeature) {
                    tokenBuffer.copyCurrentStructure(parser);
                }

            } else {
                tokenBuffer.copyCurrentStructure(parser);
            }
        }
    }

    handleBufferedTokens(parser, current, tokenBuffer);

    if (current != null && containment != null && owner != null) {
        EObjects.setOrAdd(owner, containment, current);
    }

    return current;
}

protected void handleBufferedTokens(JsonParser parser, EObject current,
        TokenBuffer tokenBuffer) throws IOException, JsonParseException {

    tokenBuffer.close();

    JsonParser bufferedParser = tokenBuffer.asParser();
    while(bufferedParser.nextToken()!=null) {
        final String fieldname = bufferedParser.getCurrentName();
        readFeature(bufferedParser, current, fieldname);
    }
}

protected boolean readFeature(JsonParser parser, EObject current,
        final String fieldname) throws JsonParseException, IOException {

    final EClass eClass = current.eClass();
    final EStructuralFeature feature = cache.getEStructuralFeature(eClass,
            fieldname);
    if (feature != null) {
        if (feature instanceof EAttribute) {
            readAttribute(parser, (EAttribute) feature, current);
        } else {
            readReference(parser, (EReference) feature, current);
        }
        return true;
    }

    return false;
}

protected void copyOldFeatures(EObject current, EObject oldCurrent) {
    EList<EStructuralFeature> allStructuralFeatures = oldCurrent.eClass().getEAllStructuralFeatures();
    for (EStructuralFeature eStructuralFeature : allStructuralFeatures) {
        if(eStructuralFeature.isChangeable()) {
            current.eSet(eStructuralFeature, oldCurrent.eGet(eStructuralFeature));
        }
    }
}

The basic idea is to place all feature JsonTokens in a ´com.fasterxml.jackson.databind.util.TokenBuffer´ until a "eClass" field is found. The possible contents of this TokenBuffer are then read and processed in a post step.

This should also work for a given currentClass (like a configured OPTION_ROOT_ELEMENT) and some given subclass in the JSON content.

What do you think?

Thanks and regards, Michael

ghillairet commented 9 years ago

Yes I am aware of this limitation that was introduced when I started to use Jackson's streaming API. I will look at your solution and fix it in the next version.

halwax commented 9 years ago

I appreciate your effort, thanks for looking into it. emfjson is a cool project and provides just the integration between emf and json that I was looking for.

Thanks and regards, Michael