incubator-isis-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Dan Haywood <...@haywood-associates.co.uk>
Subject More on the Oid refactor
Date Sat, 05 May 2012 09:13:18 GMT
Hello all,

First, just to say that I *know* the build is broken, and it's a pretty
poor show on my part to let it happen, but it's a little bit of breaking
eggs to make an omelette.

Second, wanted to update on my current thinking on OIDs ... because this is
what I'm currently working on and what I intend to commit as soon as I can
(hopefully this coming week).

Already committed:
* our OIDs are now immutable
* we have three types of OID:
   - RootOid, which is the OID of a root entity  (implementation: is
RootOidDefault).
   - AggregateOid, which is an aggregated entity that is persisted within a
root entity (eg part of a JSON doc in MongoDB).
   - CollectionOid, which wraps a List or Set of a root entity
- The RootOidDefault was refactored from SerialOid, but it now has an
"objectType" (eg "CUS" for customer), so is self-describing.
- The CollectionOid also existed previously, and wraps a List or Set for an
entity
- The AggregatedOid we also had a version of; it has a localId to
distinguish it from other aggregates of its type (within its parent root)
- Both CollectionOid and AggregatedOid have a parentOid


Not yet committed:

The first thing I've added to AggregateOid is its own objectType, and so
that has also led me to introduce a TypedOid interface: both RootOidDefault
and AggregatedOid implement TypedOid.  And so from there we can say that
parents (of either AggregateOids or of CollectionOids) are always TypedOids.

With this change to AggregateOid, we can now say that all OIDs contain
enough information to fully recreate them as objects: we do know the type
of the object to instantiate and its unique identity.  That isn't to say
there's enough information to determine how these objects are wired back
together again, but that's a responsibility of the object store to persist
this additional structural information.

~~~
Following on from this, I've been working on a string format for each of
these three types.  Currently we have numerous incompatible ways of
memo-izing OIDs: the XML object store does it one way, the NOSQL object
store another, the SQL OS object store another, the "*Mapping" classes for
the scimpi and HTML viewers (for storing the states of transient objects in
the Http session) in another, the Memento utility class in yet another, the
JSON viewer (in its URLs) yet another.  It's all very complex and messy.

Having a standard canonical string form would therefore simplify things
enormously.

I've just been writing the unit tests for this, and I've come up with the
following:

CUS:123                       - persistent root
!CUS:123                      - transient root
CUS:123#items                 - collection of persistent root
!CUS:123#items                - collection of transient root
CUS:123#NME:2                 - aggregated object within persistent root
!CUS:123~NME:2                - aggregated object within transient root
CUS:123~NME:2~CTY:LON         - aggregated object within aggregated object
within root
CUS:123~NME:2#items           - collection of an aggregated object within
root
CUS:123~NME:2~CTY:LON#streets - collection of an aggregated object within
aggregated object within root

I've pasted in the unit tests for this below, so you can see the tests in
all its glory.

Note how this allows for aggregates to be embedded within aggregates (even
though I'm not sure that any of the object stores could handle this yet).

OK, that's it for now.  Wanted to put this out there for reaction; as usual
with these things, I'll assume silence = consensus.

Dan
~~~~~~


package org.apache.isis.core.metamodel.adapter.oid;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;

/**
* <dt>CUS:123</dt>
* <dd>persistent root</dd>
* <dt>!CUS:123</dt>
* <dd>transient root</dd>
* <dt>CUS:123#items</dt>
* <dd>collection of persistent root</dd>
* <dt>!CUS:123#items</dt>
* <dd>collection of transient root</dd>
* <dt>CUS:123#NME:2</dt>
* <dd>aggregated object within persistent root</dd>
* <dt>!CUS:123~NME:2</dt>
* <dd>aggregated object within transient root</dd>
* <dt>CUS:123~NME:2~CTY:LON</dt>
* <dd>aggregated object within aggregated object within root</dd>
* <dt>CUS:123~NME:2#items</dt>
* <dd>collection of an aggregated object within root</dd>
* <dt>CUS:123~NME:2~CTY:LON#streets</dt>
* <dd>collection of an aggregated object within aggregated object within
root</dd>
*/
public class OidMarshallerTest_unmarshal {

    private OidMarshaller oidMarshaller;

    @Before
    public void setUp() throws Exception {
        oidMarshaller = new OidMarshaller();
    }

    @Test
    public void persistentRoot() {
        final String oidStr = "CUS:123";

        final RootOidDefault rootOid = oidMarshaller.unmarshal(oidStr,
RootOidDefault.class);
        assertThat(rootOid.isTransient(), is(false));
        assertThat(rootOid.getObjectType(), is("CUS"));
        assertThat(rootOid.getIdentifier(), is("123"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)rootOid));
    }

    @Test
    public void transientRoot() {
        final String oidStr = "!CUS:123";

        final RootOidDefault rootOid = oidMarshaller.unmarshal(oidStr,
RootOidDefault.class);
        assertThat(rootOid.isTransient(), is(true));
        assertThat(rootOid.getObjectType(), is("CUS"));
        assertThat(rootOid.getIdentifier(), is("123"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)rootOid));
    }

    @Test
    public void collectionOfPersistentRoot() {
        final String oidStr = "CUS:123$items";

        final CollectionOid collectionOid = oidMarshaller.unmarshal(oidStr,
CollectionOid.class);
        assertThat(collectionOid.isTransient(), is(false));
        assertThat(collectionOid.getParentOid(),
is((TypedOid)oidMarshaller.unmarshal("CUS:123", RootOidDefault.class)));
        assertThat(collectionOid.getName(), is("items"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)collectionOid));
    }

    @Test
    public void collectionOfTransientRoot() {
        final String oidStr = "!CUS:123$items";

        final CollectionOid collectionOid = oidMarshaller.unmarshal(oidStr,
CollectionOid.class);
        assertThat(collectionOid.isTransient(), is(true));
        assertThat(collectionOid.getParentOid(),
is((TypedOid)oidMarshaller.unmarshal("!CUS:123", RootOidDefault.class)));
        assertThat(collectionOid.getName(), is("items"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)collectionOid));
    }

    @Test
    public void aggregatedWithinPersistent() {
        final String oidStr = "CUS:123~NME:2";

        final AggregatedOid aggregatedOid = oidMarshaller.unmarshal(oidStr,
AggregatedOid.class);
        assertThat(aggregatedOid.isTransient(), is(false));
        assertThat(aggregatedOid.getParentOid(),
is((TypedOid)oidMarshaller.unmarshal("CUS:123", RootOidDefault.class)));
        assertThat(aggregatedOid.getObjectType(), is("NME"));
        assertThat(aggregatedOid.getLocalId(), is("2"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)aggregatedOid));
    }

    @Test
    public void aggregatedWithinTransient() {
        final String oidStr = "!CUS:123~NME:2";

        final AggregatedOid aggregatedOid = oidMarshaller.unmarshal(oidStr,
AggregatedOid.class);
        assertThat(aggregatedOid.isTransient(), is(true));
        assertThat(aggregatedOid.getParentOid(),
is((TypedOid)oidMarshaller.unmarshal("!CUS:123", RootOidDefault.class)));
        assertThat(aggregatedOid.getObjectType(), is("NME"));
        assertThat(aggregatedOid.getLocalId(), is("2"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)aggregatedOid));
    }

    @Test
    public void aggregatedWithinAggregatedWithinRoot() {
        final String oidStr = "CUS:123~ADR:2~CTY:LON";

        final AggregatedOid aggregatedOid = oidMarshaller.unmarshal(oidStr,
AggregatedOid.class);
        assertThat(aggregatedOid.isTransient(), is(false));
        assertThat(aggregatedOid.getParentOid(),
is((TypedOid)oidMarshaller.unmarshal("CUS:123~ADR:2",
AggregatedOid.class)));
        assertThat(aggregatedOid.getObjectType(), is("CTY"));
        assertThat(aggregatedOid.getLocalId(), is("LON"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)aggregatedOid));
    }

    @Test
    public void collectionOfAggregatedWithinRoot() {
        final String oidStr = "CUS:123~NME:2$items";

        final CollectionOid collectionOid = oidMarshaller.unmarshal(oidStr,
CollectionOid.class);
        assertThat(collectionOid.isTransient(), is(false));
        assertThat(collectionOid.getParentOid(),
is((TypedOid)oidMarshaller.unmarshal("CUS:123~NME:2",
AggregatedOid.class)));
        assertThat(collectionOid.getName(), is("items"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)collectionOid));
    }

    @Test
    public void collectionOfAggregatedWithinAggregatedWithinRoot() {
        final String oidStr = "CUS:123~ADR:2~CTY:LON$streets";

        final CollectionOid collectionOid = oidMarshaller.unmarshal(oidStr,
CollectionOid.class);
        assertThat(collectionOid.isTransient(), is(false));
        assertThat(collectionOid.getParentOid(),
is((TypedOid)oidMarshaller.unmarshal("CUS:123~ADR:2~CTY:LON",
AggregatedOid.class)));
        assertThat(collectionOid.getName(), is("streets"));

        final Oid oid = oidMarshaller.unmarshal(oidStr, Oid.class);
        assertThat(oid, equalTo((Oid)collectionOid));
    }


    @Test(expected=IllegalArgumentException.class)
    public void root_forCollection_oidStr() {
        oidMarshaller.unmarshal("CUS:123~NME:123$items",
RootOidDefault.class);
    }

    @Test(expected=IllegalArgumentException.class)
    public void root_forAggregated_oidStr() {
        oidMarshaller.unmarshal("CUS:123~NME:123", RootOidDefault.class);
    }

    @Test(expected=IllegalArgumentException.class)
    public void collection_forRoot_oidStr() {
        oidMarshaller.unmarshal("CUS:123", CollectionOid.class);
    }

    @Test(expected=IllegalArgumentException.class)
    public void collection_forAggregated_oidStr() {
        oidMarshaller.unmarshal("CUS:123~NME:123", CollectionOid.class);
    }


    @Test(expected=IllegalArgumentException.class)
    public void aggregated_forPersistentRoot_oidStr() {
        oidMarshaller.unmarshal("CUS:123", AggregatedOid.class);
    }

    @Test(expected=IllegalArgumentException.class)
    public void aggregated_forTransientRoot_oidStr() {
        oidMarshaller.unmarshal("!CUS:123", AggregatedOid.class);
    }

    @Test(expected=IllegalArgumentException.class)
    public void aggregated_forCollection_oidStr() {
        oidMarshaller.unmarshal("CUS:123~NME:123$items",
AggregatedOid.class);
    }

    @Test(expected=IllegalArgumentException.class)
    public void badPattern() {
        oidMarshaller.unmarshal("xxx", RootOidDefault.class);
    }


}






package org.apache.isis.core.metamodel.adapter.oid;

import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;

import org.apache.isis.core.commons.lang.CastUtils;
import org.apache.isis.core.metamodel.adapter.oid.Oid.State;

/**
 * Factory for subtypes of {@link Oid}, based on their oid str.
 *
 * <p>
 * Examples
 * <dl>
 * <dt>CUS:123</dt>
 * <dd>persistent root</dd>
 * <dt>T~CUS:123</dt>
 * <dd>transient root</dd>
 * <dt>CUS:123#items</dt>
 * <dd>collecion of persistent root</dd>
 * <dt>T~CUS:123#items</dt>
 * <dd>collecion of transient root</dd>
 * <dt>CUS:123#NME:2</dt>
 * <dd>aggregated object within persistent root</dd>
 * <dt>T~CUS:123#NME:2</dt>
 * <dd>aggregated object within transient root</dd>
 * <dt>CUS:123#NME:2#CTY:LON</dt>
 * <dd>aggregated object within aggregated object within root</dd>
 * <dt>CUS:123#NME:2#items</dt>
 * <dd>collection of an aggregated object within root</dd>
 * <dt>CUS:123#NME:2#CTY:LON#streets</dt>
 * <dd>collection of an aggregated object within aggregated object within
root</dd>
 * </dl>
 *
 * <p>
 * Separators:
 * <dl>
 * <dt>!</dt>
 * <dd>precedes root object type, indicates transient</dd>
 * <dt>:</dt>
 * <dd>precedes root object identifier</dd>
 * <dt>~</dt>
 * <dd>precedes aggregate oid</dd>
 * <dt>$</dt>
 * <dd>precedes collection name</dd>
 * </dl>
 * <p>
 * Note that # and ; were not chosen as separators to minimize noise when
URL encoding OIDs.
 */
public class OidMarshaller {

    private static Pattern OIDSTR_PATTERN =

Pattern.compile("^((([!])?([^:@#$]+):([^:@#$]+))((~[^:@#$]+:[^:@#$]+)*))([$]([^:@#$]+))?$");

    ////////////////////////////////////////////////////////////////
    // unmarshall
    ////////////////////////////////////////////////////////////////

    public <T extends Oid> T unmarshal(String oidStr, Class<T>
requestedType) {

        final Matcher matcher = OIDSTR_PATTERN.matcher(oidStr);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("Could not parse OID '" +
oidStr + "'; should match pattern: " + OIDSTR_PATTERN.pattern());
        }

        final int groupCount = matcher.groupCount();

        final String isTransientStr = getGroup(matcher, 3);
        boolean isTransient = "!".equals(isTransientStr);

        final String oidStrWithoutCollectionName = getGroup(matcher, 1);
        final String rootOidStr = getGroup(matcher, 2);

        final String rootObjectType = getGroup(matcher, 4);
        final String rootIdentifier = getGroup(matcher, 5);

        final String aggregateOidPart = getGroup(matcher, 6);
        final List<AggregateOidPart> aggregateOidParts =
Lists.newArrayList();
        final Splitter tildaSplitter = Splitter.on("~");
        final Splitter colonSplitter = Splitter.on(":");
        if(aggregateOidPart != null) {
            final Iterable<String> tildaSplitIter =
tildaSplitter.split(aggregateOidPart);
            for(String str: tildaSplitIter) {
                if(Strings.isNullOrEmpty(str)) {
                    continue; // leading "~"
                }
                final Iterator<String> colonSplitIter =
colonSplitter.split(str).iterator();
                final String objectType = colonSplitIter.next();
                final String localId = colonSplitIter.next();
                aggregateOidParts.add(new AggregateOidPart(objectType,
localId));
            }
        }
        final String collectionName = getGroup(matcher, groupCount); //
last one

        if(collectionName == null) {
            if(aggregateOidParts.isEmpty()) {
                ensureCorrectType(oidStr, requestedType,
RootOidDefault.class);
                return CastUtils.cast(new RootOidDefault(rootObjectType,
rootIdentifier, State.valueOf(isTransient)));
            } else {
                ensureCorrectType(oidStr, requestedType,
AggregatedOid.class);
                final AggregateOidPart lastPart =
aggregateOidParts.remove(aggregateOidParts.size()-1);
                final TypedOid parentOid = parentOidFor(rootOidStr,
aggregateOidParts);
                return CastUtils.cast(new
AggregatedOid(lastPart.objectType, parentOid, lastPart.localId));
            }
        } else {
            TypedOid parentOid =
this.unmarshal(oidStrWithoutCollectionName, TypedOid.class);
            ensureCorrectType(oidStr, requestedType, CollectionOid.class);
            return CastUtils.cast(new CollectionOid(parentOid,
collectionName));
        }
    }

    private static class AggregateOidPart {
        AggregateOidPart(String objectType, String localId) {
            this.objectType = objectType;
            this.localId = localId;
        }
        String objectType;
        String localId;
        public String toString() {
            return "~" + objectType + ":" + localId;
        }
    }


    private TypedOid parentOidFor(final String rootOidStr, final
List<AggregateOidPart> aggregateOidParts) {
        final StringBuilder buf = new StringBuilder(rootOidStr);
        for(AggregateOidPart part: aggregateOidParts) {
            buf.append(part.toString());
        }
        return unmarshal(buf.toString(), TypedOid.class);
    }

    private <T> void ensureCorrectType(String oidStr, Class<T>
requestedType, final Class<? extends Oid> actualType) {
        if(!requestedType.isAssignableFrom(actualType)) {
            throw new IllegalArgumentException("OID '" + oidStr + "' does
not represent a " +
            actualType.getSimpleName());
        }
    }

    private String getGroup(final Matcher matcher, final int group) {
        final int groupCount = matcher.groupCount();
        if(group > groupCount) {
            return null;
        }
        final String val = matcher.group(group);
        return Strings.emptyToNull(val);
    }


    ////////////////////////////////////////////////////////////////
    // marshall
    ////////////////////////////////////////////////////////////////

    public String marshal(RootOidDefault rootOid) {
        return (rootOid.isTransient()? "!" : "") + rootOid.getObjectType()
+ ":" + rootOid.getIdentifier();
    }

    public String marshal(CollectionOid collectionOid) {
        return collectionOid.getParentOid().enString() + "$" +
collectionOid.getName();
    }

    public String marshal(AggregatedOid aggregatedOid) {
        return aggregatedOid.getParentOid().enString() + "~" +
aggregatedOid.getObjectType() + ":" + aggregatedOid.getLocalId();
    }


}

Mime
  • Unnamed multipart/alternative (inline, None, 0 bytes)
View raw message