Hi Dan, Thanks for the update, good work, worth eating a few bits of eggshell:-)
Could you list/ clarify meanings of your canonical string for symbols #~! etc please- I'm confused by difference between ~ and # eg CUS:123~NME:2~CTY:LON Mike Burton ( iPhone) On 5 May 2012, at 10:13, Dan Haywood <[email protected]> wrote: > 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(); > } > > > }
