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}