/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ 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.applib.annotation.Programmatic; import org.apache.isis.applib.services.bookmark.Bookmark; import org.apache.isis.core.metamodel.adapter.oid.Oid.State; import org.apache.isis.core.metamodel.adapter.version.Version; import org.apache.isis.core.metamodel.spec.ObjectSpecId; /** * Factory for subtypes of {@link Oid}, based on their oid str. * * <p> * Examples * <dl> * <dt>CUS:123</dt> * <dd>persistent root</dd> * <dt>!CUS:123</dt> * <dd>transient root</dd> * <dt>*CUS:123</dt> * <dd>view model 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> * </dl> * * <p> * Separators: * <dl> * <dt>!</dt> * <dd>precedes root object type, indicates transient</dd> * <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> * <dt>^</dt> * <dd>precedes version</dd> * </dl> * * <p> * Note that # and ; were not chosen as separators to minimize noise when URL encoding OIDs. */ public final class OidMarshaller { public final static OidMarshaller INSTANCE = new OidMarshaller(); private OidMarshaller(){} //region > public constants public static final String VIEWMODEL_INDICATOR = Bookmark.ObjectState.VIEW_MODEL.getCode(); // "*" //endregion //region > private constants private static final String TRANSIENT_INDICATOR = Bookmark.ObjectState.TRANSIENT.getCode() ; // "!" private static final String SEPARATOR = ":"; private static final String SEPARATOR_NESTING = "~"; private static final String SEPARATOR_COLLECTION = "$"; private static final String SEPARATOR_VERSION = "^"; private static final String WORD = "[^" + SEPARATOR + SEPARATOR_NESTING + SEPARATOR_COLLECTION + "\\" + SEPARATOR_VERSION + "#" + "]+"; private static final String DIGITS = "\\d+"; private static final String WORD_GROUP = "(" + WORD + ")"; private static final String DIGITS_GROUP = "(" + DIGITS + ")"; private static Pattern OIDSTR_PATTERN = Pattern.compile( "^(" + "(" + "([" + TRANSIENT_INDICATOR + VIEWMODEL_INDICATOR + "])?" + WORD_GROUP + SEPARATOR + WORD_GROUP + ")" + "(" + "(" + SEPARATOR_NESTING + WORD + SEPARATOR + WORD + ")*" + // nesting of aggregates ")" + ")" + "(" + "[" + SEPARATOR_COLLECTION + "]" + WORD + ")?" + // optional collection name "(" + "[\\" + SEPARATOR_VERSION + "]" + DIGITS_GROUP + // optional version digit SEPARATOR + WORD_GROUP + "?" + // optional version user name SEPARATOR + DIGITS_GROUP + "?" + // optional version UTC time ")?" + "$"); //endregion //region > join, split @Programmatic public String joinAsOid(String domainType, String instanceId) { return domainType + SEPARATOR + instanceId; } @Programmatic public String splitInstanceId(String oidStr) { final int indexOfSeperator = oidStr.indexOf(SEPARATOR); return indexOfSeperator > 0? oidStr.substring(indexOfSeperator+1): null; } //endregion //region > unmarshal @Programmatic @SuppressWarnings("unchecked") 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 String isTransientOrViewModelStr = getGroup(matcher, 3); final State state; if("!".equals(isTransientOrViewModelStr)) { state = State.TRANSIENT; } else if("*".equals(isTransientOrViewModelStr)) { state = State.VIEWMODEL; } else { state = State.PERSISTENT; } 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 nestingSplitter = Splitter.on(SEPARATOR_NESTING); final Splitter partsSplitter = Splitter.on(SEPARATOR); if(aggregateOidPart != null) { final Iterable<String> tildaSplitIter = nestingSplitter.split(aggregateOidPart); for(String str: tildaSplitIter) { if(Strings.isNullOrEmpty(str)) { continue; // leading "~" } final Iterator<String> colonSplitIter = partsSplitter.split(str).iterator(); final String objectType = colonSplitIter.next(); final String localId = colonSplitIter.next(); aggregateOidParts.add(new AggregateOidPart(objectType, localId)); } } final String collectionPart = getGroup(matcher, 8); final String collectionName = collectionPart != null ? collectionPart.substring(1) : null; final String versionSequence = getGroup(matcher, 10); final String versionUser = getGroup(matcher, 11); final String versionUtcTimestamp = getGroup(matcher, 12); final Version version = Version.create(versionSequence, versionUser, versionUtcTimestamp); if(collectionName == null) { if(aggregateOidParts.isEmpty()) { ensureCorrectType(oidStr, requestedType, RootOid.class); return (T) new RootOid(ObjectSpecId.of(rootObjectType), rootIdentifier, state, version); } else { throw new RuntimeException("Aggregated Oids are no longer supported"); } } else { final String oidStrWithoutCollectionName = getGroup(matcher, 1); final String parentOidStr = oidStrWithoutCollectionName + marshal(version); RootOid parentOid = this.unmarshal(parentOidStr, RootOid.class); ensureCorrectType(oidStr, requestedType, ParentedCollectionOid.class); return (T)new ParentedCollectionOid(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 SEPARATOR_NESTING + objectType + SEPARATOR + localId; } } 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); } //endregion //region > marshal @Programmatic public final String marshal(RootOid rootOid) { return marshalNoVersion(rootOid) + marshal(rootOid.getVersion()); } @Programmatic public final String marshalNoVersion(RootOid rootOid) { final String transientIndicator = rootOid.isTransient()? TRANSIENT_INDICATOR : ""; final String viewModelIndicator = rootOid.isViewModel()? VIEWMODEL_INDICATOR : ""; return transientIndicator + viewModelIndicator + rootOid.getObjectSpecId() + SEPARATOR + rootOid.getIdentifier(); } @Programmatic public final String marshal(ParentedCollectionOid collectionOid) { return marshalNoVersion(collectionOid) + marshal(collectionOid.getVersion()); } @Programmatic public String marshalNoVersion(ParentedCollectionOid collectionOid) { return collectionOid.getRootOid().enStringNoVersion() + SEPARATOR_COLLECTION + collectionOid.getName(); } @Programmatic public final String marshal(Version version) { if(version == null) { return ""; } final String versionUser = version.getUser(); return SEPARATOR_VERSION + version.getSequence() + SEPARATOR + Strings.nullToEmpty(versionUser) + SEPARATOR + nullToEmpty(version.getUtcTimestamp()); } private static String nullToEmpty(Object obj) { return obj == null? "": "" + obj; } //endregion }