001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.isis.core.metamodel.adapter.oid; 020 021import java.util.Iterator; 022import java.util.List; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import com.google.common.base.Splitter; 027import com.google.common.base.Strings; 028import com.google.common.collect.Lists; 029 030import org.apache.isis.core.metamodel.adapter.oid.Oid.State; 031import org.apache.isis.core.metamodel.adapter.version.Version; 032import org.apache.isis.core.metamodel.spec.ObjectSpecId; 033 034/** 035 * Factory for subtypes of {@link Oid}, based on their oid str. 036 * 037 * <p> 038 * Examples 039 * <dl> 040 * <dt>CUS:123</dt> 041 * <dd>persistent root</dd> 042 * <dt>!CUS:123</dt> 043 * <dd>transient root</dd> 044 * <dt>*CUS:123</dt> 045 * <dd>view model root</dd> 046 * <dt>CUS:123$items</dt> 047 * <dd>collection of persistent root</dd> 048 * <dt>!CUS:123$items</dt> 049 * <dd>collection of transient root</dd> 050 * <dt>CUS:123~NME:2</dt> 051 * <dd>aggregated object within persistent root</dd> 052 * <dt>!CUS:123~NME:2</dt> 053 * <dd>aggregated object within transient root</dd> 054 * <dt>CUS:123~NME:2~CTY:LON</dt> 055 * <dd>aggregated object within aggregated object within root</dd> 056 * <dt>CUS:123~NME:2$items</dt> 057 * <dd>collection of an aggregated object within root</dd> 058 * <dt>CUS:123~NME:2~CTY:LON$streets</dt> 059 * <dd>collection of an aggregated object within aggregated object within root</dd> 060 * </dl> 061 * 062 * <p> 063 * Separators: 064 * <dl> 065 * <dt>!</dt> 066 * <dd>precedes root object type, indicates transient</dd> 067 * <dt>*</dt> 068 * <dd>precedes root object type, indicates transient</dd> 069 * <dt>:</dt> 070 * <dd>precedes root object identifier</dd> 071 * <dt>~</dt> 072 * <dd>precedes aggregate oid</dd> 073 * <dt>$</dt> 074 * <dd>precedes collection name</dd> 075 * <dt>^</dt> 076 * <dd>precedes version</dd> 077 * </dl> 078 * 079 * <p> 080 * Note that # and ; were not chosen as separators to minimize noise when URL encoding OIDs. 081 */ 082public class OidMarshaller { 083 084 private static final String TRANSIENT_INDICATOR = "!"; 085 public static final String VIEWMODEL_INDICATOR = "*"; 086 private static final String SEPARATOR = ":"; 087 private static final String SEPARATOR_NESTING = "~"; 088 private static final String SEPARATOR_COLLECTION = "$"; 089 private static final String SEPARATOR_VERSION = "^"; 090 091 private static final String WORD = "[^" + SEPARATOR + SEPARATOR_NESTING + SEPARATOR_COLLECTION + "\\" + SEPARATOR_VERSION + "#" + "]+"; 092 private static final String DIGITS = "\\d+"; 093 094 private static final String WORD_GROUP = "(" + WORD + ")"; 095 private static final String DIGITS_GROUP = "(" + DIGITS + ")"; 096 097 private static Pattern OIDSTR_PATTERN = 098 Pattern.compile( 099 "^(" + 100 "(" + 101 "([" + TRANSIENT_INDICATOR + VIEWMODEL_INDICATOR + "])?" + 102 WORD_GROUP + SEPARATOR + WORD_GROUP + 103 ")" + 104 "(" + 105 "(" + SEPARATOR_NESTING + WORD + SEPARATOR + WORD + ")*" + // nesting of aggregates 106 ")" + 107 ")" + 108 "(" + "[" + SEPARATOR_COLLECTION + "]" + WORD + ")?" + // optional collection name 109 "(" + 110 "[\\" + SEPARATOR_VERSION + "]" + 111 DIGITS_GROUP + // optional version digit 112 SEPARATOR + WORD_GROUP + "?" + // optional version user name 113 SEPARATOR + DIGITS_GROUP + "?" + // optional version UTC time 114 ")?" + 115 "$"); 116 117 //////////////////////////////////////////////////////////////// 118 // constructor 119 //////////////////////////////////////////////////////////////// 120 121 public OidMarshaller() {} 122 123 124 //////////////////////////////////////////////////////////////// 125 // join, split 126 //////////////////////////////////////////////////////////////// 127 128 public String joinAsOid(String domainType, String instanceId) { 129 return domainType + SEPARATOR + instanceId; 130 } 131 132 public String splitInstanceId(String oidStr) { 133 final int indexOfSeperator = oidStr.indexOf(SEPARATOR); 134 return indexOfSeperator > 0? oidStr.substring(indexOfSeperator+1): null; 135 } 136 137 //////////////////////////////////////////////////////////////// 138 // unmarshal 139 //////////////////////////////////////////////////////////////// 140 141 @SuppressWarnings("unchecked") 142 public <T extends Oid> T unmarshal(String oidStr, Class<T> requestedType) { 143 144 final Matcher matcher = OIDSTR_PATTERN.matcher(oidStr); 145 if (!matcher.matches()) { 146 throw new IllegalArgumentException("Could not parse OID '" + oidStr + "'; should match pattern: " + OIDSTR_PATTERN.pattern()); 147 } 148 149 final String isTransientOrViewModelStr = getGroup(matcher, 3); 150 final State state; 151 if("!".equals(isTransientOrViewModelStr)) { 152 state = State.TRANSIENT; 153 } else if("*".equals(isTransientOrViewModelStr)) { 154 state = State.VIEWMODEL; 155 } else { 156 state = State.PERSISTENT; 157 } 158 159 final String rootOidStr = getGroup(matcher, 2); 160 161 final String rootObjectType = getGroup(matcher, 4); 162 final String rootIdentifier = getGroup(matcher, 5); 163 164 final String aggregateOidPart = getGroup(matcher, 6); 165 final List<AggregateOidPart> aggregateOidParts = Lists.newArrayList(); 166 final Splitter nestingSplitter = Splitter.on(SEPARATOR_NESTING); 167 final Splitter partsSplitter = Splitter.on(SEPARATOR); 168 if(aggregateOidPart != null) { 169 final Iterable<String> tildaSplitIter = nestingSplitter.split(aggregateOidPart); 170 for(String str: tildaSplitIter) { 171 if(Strings.isNullOrEmpty(str)) { 172 continue; // leading "~" 173 } 174 final Iterator<String> colonSplitIter = partsSplitter.split(str).iterator(); 175 final String objectType = colonSplitIter.next(); 176 final String localId = colonSplitIter.next(); 177 aggregateOidParts.add(new AggregateOidPart(objectType, localId)); 178 } 179 } 180 final String collectionPart = getGroup(matcher, 8); 181 final String collectionName = collectionPart != null ? collectionPart.substring(1) : null; 182 183 final String versionSequence = getGroup(matcher, 10); 184 final String versionUser = getGroup(matcher, 11); 185 final String versionUtcTimestamp = getGroup(matcher, 12); 186 final Version version = Version.create(versionSequence, versionUser, versionUtcTimestamp); 187 188 if(collectionName == null) { 189 if(aggregateOidParts.isEmpty()) { 190 ensureCorrectType(oidStr, requestedType, RootOidDefault.class); 191 return (T)new RootOidDefault(ObjectSpecId.of(rootObjectType), rootIdentifier, state, version); 192 } else { 193 ensureCorrectType(oidStr, requestedType, AggregatedOid.class); 194 final AggregateOidPart lastPart = aggregateOidParts.remove(aggregateOidParts.size()-1); 195 final TypedOid parentOid = parentOidFor(rootOidStr, aggregateOidParts, version); 196 return (T)new AggregatedOid(ObjectSpecId.of(lastPart.objectType), parentOid, lastPart.localId); 197 } 198 } else { 199 final String oidStrWithoutCollectionName = getGroup(matcher, 1); 200 201 final String parentOidStr = oidStrWithoutCollectionName + marshal(version); 202 203 TypedOid parentOid = this.unmarshal(parentOidStr, TypedOid.class); 204 ensureCorrectType(oidStr, requestedType, CollectionOid.class); 205 return (T)new CollectionOid(parentOid, collectionName); 206 } 207 } 208 209 210 211 private static class AggregateOidPart { 212 AggregateOidPart(String objectType, String localId) { 213 this.objectType = objectType; 214 this.localId = localId; 215 } 216 String objectType; 217 String localId; 218 public String toString() { 219 return SEPARATOR_NESTING + objectType + SEPARATOR + localId; 220 } 221 } 222 223 224 private TypedOid parentOidFor(final String rootOidStr, final List<AggregateOidPart> aggregateOidParts, Version version) { 225 final StringBuilder buf = new StringBuilder(rootOidStr); 226 for(AggregateOidPart part: aggregateOidParts) { 227 buf.append(part.toString()); 228 } 229 buf.append(marshal(version)); 230 return unmarshal(buf.toString(), TypedOid.class); 231 } 232 233 private <T> void ensureCorrectType(String oidStr, Class<T> requestedType, final Class<? extends Oid> actualType) { 234 if(!requestedType.isAssignableFrom(actualType)) { 235 throw new IllegalArgumentException("OID '" + oidStr + "' does not represent a " + 236 actualType.getSimpleName()); 237 } 238 } 239 240 private String getGroup(final Matcher matcher, final int group) { 241 final int groupCount = matcher.groupCount(); 242 if(group > groupCount) { 243 return null; 244 } 245 final String val = matcher.group(group); 246 return Strings.emptyToNull(val); 247 } 248 249 250 //////////////////////////////////////////////////////////////// 251 // marshal 252 //////////////////////////////////////////////////////////////// 253 254 public final String marshal(RootOid rootOid) { 255 return marshalNoVersion(rootOid) + marshal(rootOid.getVersion()); 256 } 257 258 public final String marshalNoVersion(RootOid rootOid) { 259 final String transientIndicator = rootOid.isTransient()? TRANSIENT_INDICATOR : ""; 260 final String viewModelIndicator = rootOid.isViewModel()? VIEWMODEL_INDICATOR : ""; 261 return transientIndicator + viewModelIndicator + rootOid.getObjectSpecId() + SEPARATOR + rootOid.getIdentifier(); 262 } 263 264 public final String marshal(CollectionOid collectionOid) { 265 return marshalNoVersion(collectionOid) + marshal(collectionOid.getVersion()); 266 } 267 268 public String marshalNoVersion(CollectionOid collectionOid) { 269 return collectionOid.getParentOid().enStringNoVersion(this) + SEPARATOR_COLLECTION + collectionOid.getName(); 270 } 271 272 public final String marshal(AggregatedOid aggregatedOid) { 273 return marshalNoVersion(aggregatedOid) + marshal(aggregatedOid.getVersion()); 274 } 275 276 public final String marshalNoVersion(AggregatedOid aggregatedOid) { 277 return aggregatedOid.getParentOid().enStringNoVersion(this) + SEPARATOR_NESTING + aggregatedOid.getObjectSpecId() + SEPARATOR + aggregatedOid.getLocalId(); 278 } 279 280 public final String marshal(Version version) { 281 if(version == null) { 282 return ""; 283 } 284 final String versionUser = version.getUser(); 285 return SEPARATOR_VERSION + version.getSequence() + SEPARATOR + Strings.nullToEmpty(versionUser) + SEPARATOR + nullToEmpty(version.getUtcTimestamp()); 286 } 287 288 289 private static String nullToEmpty(Object obj) { 290 return obj == null? "": "" + obj; 291 } 292 293}