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 */
019
020package org.apache.isis.core.metamodel.layout;
021
022import java.util.ArrayList;
023import java.util.Iterator;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Set;
027import java.util.SortedMap;
028import java.util.SortedSet;
029import java.util.StringTokenizer;
030import java.util.TreeSet;
031
032import com.google.common.collect.Lists;
033import com.google.common.collect.Maps;
034import com.google.common.collect.Sets;
035
036import org.apache.isis.core.metamodel.facetapi.FacetHolder;
037import org.apache.isis.core.metamodel.facetapi.IdentifiedHolder;
038import org.apache.isis.core.metamodel.facets.FacetedMethod;
039import org.apache.isis.core.metamodel.facets.members.order.MemberOrderFacet;
040import org.apache.isis.core.metamodel.layout.memberorderfacet.MemberIdentifierComparator;
041import org.apache.isis.core.metamodel.layout.memberorderfacet.MemberOrderComparator;
042
043/**
044 * Represents a nested hierarchy of ordered members.
045 * 
046 * <p>
047 * At each level the elements are either {@link FacetedMethod}s or they are
048 * instances of {@link OrderSet} represent a group of {@link FacetedMethod}s
049 * that have a {@link MemberOrderFacet} of the same name.
050 * 
051 * <p>
052 * With no name, (ie <tt>name=""</tt> is the default), at the top level
053 * 
054 * <pre>
055 * MemberOrder(sequence=&quot;1&quot;)
056 * MemberOrder(sequence=&quot;1.1&quot;)
057 * MemberOrder(sequence=&quot;1.2&quot;)
058 * MemberOrder(sequence=&quot;1.2.1&quot;)
059 * MemberOrder(sequence=&quot;1.3&quot;)
060 * </pre>
061 * 
062 * <p>
063 * With names, creates a hierarchy.
064 * 
065 * <pre>
066 * MemberOrder(sequence=&quot;1.1&quot;)                   // no parent
067 * MemberOrder(sequence=&quot;1.2.1&quot;)
068 * MemberOrder(sequence=&quot;1.3&quot;)
069 * MemberOrder(name=&quot;abc&quot;, sequence=&quot;1&quot;)         // group is abc, parent is &quot;&quot;
070 * MemberOrder(name=&quot;abc&quot;, sequence=&quot;1.2&quot;)
071 * MemberOrder(name=&quot;abc,def&quot;, sequence=&quot;1&quot;)     // group is def, parent is abc
072 * MemberOrder(name=&quot;abc,def&quot;, sequence=&quot;1.2&quot;)
073 * </pre>
074 * 
075 */
076public class DeweyOrderSet implements Comparable<DeweyOrderSet>, Iterable<Object>  {
077    
078    public static DeweyOrderSet createOrderSet(final List<? extends IdentifiedHolder> identifiedHolders) {
079
080        final SortedMap<String, SortedSet<IdentifiedHolder>> sortedMembersByGroup = Maps.newTreeMap();
081        final SortedSet<IdentifiedHolder> nonAnnotatedGroup = Sets.newTreeSet(new MemberIdentifierComparator());
082
083        // spin over all the members and put them into a Map of SortedSets
084        // any non-annotated members go into additional nonAnnotatedGroup set.
085        for (final IdentifiedHolder identifiedHolder : identifiedHolders) {
086            final MemberOrderFacet memberOrder = identifiedHolder.getFacet(MemberOrderFacet.class);
087            if (memberOrder == null) {
088                nonAnnotatedGroup.add(identifiedHolder);
089                continue;
090            }
091            final SortedSet<IdentifiedHolder> sortedMembersForGroup = getSortedSet(sortedMembersByGroup, memberOrder.name());
092            sortedMembersForGroup.add(identifiedHolder);
093        }
094
095        // add the non-annotated group to the first "" group.
096        final SortedSet<IdentifiedHolder> defaultSet = getSortedSet(sortedMembersByGroup, "");
097        defaultSet.addAll(nonAnnotatedGroup);
098
099        // create OrderSets, wiring up parents and children.
100
101        // since sortedMembersByGroup is a SortedMap, the
102        // iteration will be in alphabetical order (ie parent groups before
103        // their children).
104        final Set<String> groupNames = sortedMembersByGroup.keySet();
105        final SortedMap<String, DeweyOrderSet> orderSetsByGroup = Maps.newTreeMap();
106
107        for (final String string : groupNames) {
108            final String groupName = string;
109            final DeweyOrderSet deweyOrderSet = new DeweyOrderSet(groupName);
110            orderSetsByGroup.put(groupName, deweyOrderSet);
111            ensureParentFor(orderSetsByGroup, deweyOrderSet);
112        }
113
114        // now populate the OrderSets
115        for (final String groupName : groupNames) {
116            final DeweyOrderSet deweyOrderSet = orderSetsByGroup.get(groupName);
117            // REVIEW: something fishy happens here with casting, hence warnings
118            // left in
119            final SortedSet sortedMembers = sortedMembersByGroup.get(groupName);
120            deweyOrderSet.addAll(sortedMembers);
121            deweyOrderSet.copyOverChildren();
122        }
123
124        return orderSetsByGroup.get("");
125    }
126
127    /**
128     * Recursively creates parents all the way up to root (<tt>""</tt>), along
129     * the way associating each child with its parent and adding the child as an
130     * element of its parent.
131     * 
132     * @param orderSetsByGroup
133     * @param deweyOrderSet
134     */
135    private static void ensureParentFor(final SortedMap<String,DeweyOrderSet> orderSetsByGroup, final DeweyOrderSet deweyOrderSet) {
136        final String parentGroup = deweyOrderSet.getGroupPath();
137        DeweyOrderSet parentOrderSet = (DeweyOrderSet) orderSetsByGroup.get(parentGroup);
138        if (parentOrderSet == null) {
139            parentOrderSet = new DeweyOrderSet(parentGroup);
140            orderSetsByGroup.put(parentGroup, parentOrderSet);
141            if (!parentGroup.equals("")) {
142                ensureParentFor(orderSetsByGroup, deweyOrderSet);
143            }
144        }
145        // check in case at root
146        if (deweyOrderSet != parentOrderSet) {
147            deweyOrderSet.setParent(parentOrderSet);
148            parentOrderSet.addChild(deweyOrderSet);
149        }
150    }
151
152    /**
153     * Gets the SortedSet with the specified group from the supplied Map of
154     * SortedSets.
155     * 
156     * <p>
157     * If there is no such SortedSet, creates.
158     * 
159     * @param sortedMembersByGroup
160     * @param groupName
161     * @return
162     */
163    private static SortedSet<IdentifiedHolder> getSortedSet(final SortedMap<String, SortedSet<IdentifiedHolder>> sortedMembersByGroup, final String groupName) {
164        SortedSet<IdentifiedHolder> sortedMembersForGroup = sortedMembersByGroup.get(groupName);
165        if (sortedMembersForGroup == null) {
166            sortedMembersForGroup = new TreeSet<IdentifiedHolder>(new MemberOrderComparator(true));
167            sortedMembersByGroup.put(groupName, sortedMembersForGroup);
168        }
169        return sortedMembersForGroup;
170    }
171
172    // /////////////////////////////////////////////////////////////////////////
173    
174    private final List<Object> elements = Lists.newArrayList();
175    private final String groupFullName;
176    private final String groupName;
177    private final String groupPath;
178
179    private DeweyOrderSet parent;
180
181    /**
182     * A staging area until we are ready to add the child sets to the collection
183     * of elements owned by the superclass.
184     */
185    protected SortedSet<DeweyOrderSet> childOrderSets = new TreeSet<DeweyOrderSet>();
186
187    // /////////////////////////////////////////////////////////////////////////
188
189    private DeweyOrderSet(final String groupFullName) {
190        this.groupFullName = groupFullName;
191
192        groupName = deriveGroupName(groupFullName);
193        groupPath = deriveGroupPath(groupFullName);
194    }
195
196    // ///////////////// Group Name etc ////////////////////
197
198    /**
199     * Last component of the comma-separated group name supplied in the
200     * constructor (analogous to the file name extracted from a fully qualified
201     * file name).
202     * 
203     * <p>
204     * For example, if supplied <tt>abc,def,ghi</tt> in the constructor, then
205     * this will return <tt>ghi</tt>.
206     */
207    public String getGroupName() {
208        return groupName;
209    }
210
211    /**
212     * The group name exactly as it was supplied in the constructor (analogous
213     * to a fully qualified file name).
214     * 
215     * <p>
216     * For example, if supplied <tt>abc,def,ghi</tt> in the constructor, then
217     * this will return the same string <tt>abc,def,ghi</tt>.
218     */
219    public String getGroupFullName() {
220        return groupFullName;
221    }
222
223    /**
224     * Represents the parent groups, derived from the group name supplied in the
225     * constructor (analogous to the directory portion of a fully qualified file
226     * name).
227     * 
228     * <p>
229     * For example, if supplied <tt>abc,def,ghi</tt> in the constructor, then
230     * this will return <tt>abc,def</tt>.
231     */
232    public String getGroupPath() {
233        return groupPath;
234    }
235
236    /**
237     * Splits name by comma, then title case the last component.
238     */
239    private static String deriveGroupName(final String groupFullName) {
240        final StringTokenizer tokens = new StringTokenizer(groupFullName, ",", false);
241        final String[] groupNameComponents = new String[tokens.countTokens()];
242        for (int i = 0; tokens.hasMoreTokens(); i++) {
243            groupNameComponents[i] = tokens.nextToken();
244        }
245        final String groupSimpleName = groupNameComponents.length > 0 ? groupNameComponents[groupNameComponents.length - 1] : "";
246        if (groupSimpleName.length() > 1) {
247            return groupSimpleName.substring(0, 1).toUpperCase() + groupSimpleName.substring(1);
248        } else {
249            return groupSimpleName.toUpperCase();
250        }
251
252    }
253
254    /**
255     * Everything up to the last comma, else empty string if none.
256     */
257    private static String deriveGroupPath(final String groupFullName) {
258        final int lastComma = groupFullName.lastIndexOf(",");
259        if (lastComma == -1) {
260            return "";
261        }
262        return groupFullName.substring(0, lastComma);
263    }
264
265    // ///////////////////// Parent & Children ///////////////////
266
267    protected void setParent(final DeweyOrderSet parent) {
268        this.parent = parent;
269    }
270
271    public DeweyOrderSet getParent() {
272        return parent;
273    }
274
275    protected void addChild(final DeweyOrderSet childOrderSet) {
276        childOrderSets.add(childOrderSet);
277    }
278
279    public List<DeweyOrderSet> children() {
280        final ArrayList<DeweyOrderSet> list = new ArrayList<DeweyOrderSet>();
281        list.addAll(childOrderSets);
282        return list;
283    }
284
285    protected void copyOverChildren() {
286        addAll(childOrderSets);
287    }
288
289    // ///////////////////// Elements (includes children) ///////////////////
290
291    /**
292     * Returns a copy of the elements, in sequence.
293     */
294    public List<Object> elementList() {
295        return new ArrayList<Object>(elements);
296    }
297
298    public int size() {
299        return elements.size();
300    }
301
302    protected void addElement(final Object element) {
303        elements.add(element);
304    }
305
306    @Override
307    public Iterator<Object> iterator() {
308        return elements.iterator();
309    }
310
311    protected void addAll(final SortedSet<?> sortedMembers) {
312        for (final Object deweyOrderSet : sortedMembers) {
313            this.addElement(deweyOrderSet);
314        }
315    }
316    
317    // ///////////////////////// reorderChildren //////////////////////
318    
319    public void reorderChildren(List<String> requiredOrder) {
320        final LinkedHashMap<String,DeweyOrderSet> orderSets = Maps.newLinkedHashMap();
321        
322        // remove all OrderSets from elements
323        // though remembering the order they were encountered
324        for (Object child : elementList()) {
325            if(child instanceof DeweyOrderSet) {
326                final DeweyOrderSet orderSet = (DeweyOrderSet) child;
327                elements.remove(orderSet);
328                orderSets.put(orderSet.getGroupName(), orderSet);
329            }
330        }
331        
332        // spin through the requiredOrder and add back in (if found)  
333        for (String group : requiredOrder) {
334            DeweyOrderSet orderSet = orderSets.get(group);
335            if(orderSet == null) {
336                continue;
337            }
338            orderSets.remove(group);
339            elements.add(orderSet);
340        }
341        
342        // anything left, add back in the original order
343        for (String orderSetGroupName : orderSets.keySet()) {
344            final DeweyOrderSet orderSet = orderSets.get(orderSetGroupName);
345            elements.add(orderSet);
346        }
347    }
348
349
350    // //////////////////////////////////////
351    // compareTo, equals, hashCode, toString
352    // //////////////////////////////////////
353
354    /**
355     * Natural ordering is to compare by {@link #getGroupFullName()}.
356     */
357    @Override
358    public int compareTo(final DeweyOrderSet o) {
359        if (this.equals(o)) {
360            return 0;
361        }
362        return groupFullName.compareTo(o.groupFullName);
363    }
364
365    @Override
366    public boolean equals(final Object obj) {
367        if (this == obj) {
368            return true;
369        }
370        if (obj == null) {
371            return false;
372        }
373        if (getClass() != obj.getClass()) {
374            return false;
375        }
376        final DeweyOrderSet other = (DeweyOrderSet) obj;
377        if (groupFullName == null) {
378            if (other.groupFullName != null) {
379                return false;
380            }
381        } else if (!groupFullName.equals(other.groupFullName)) {
382            return false;
383        }
384        return true;
385    }
386
387    @Override
388    public int hashCode() {
389        final int prime = 31;
390        int result = 1;
391        result = prime * result + ((groupFullName == null) ? 0 : groupFullName.hashCode());
392        return result;
393    }
394
395    /**
396     * Format is: <tt>abc,def:XXel/YYm/ZZch</tt>
397     * <p>
398     * where <tt>abc,def</tt> is group name, <tt>XX</tt> is number of elements,
399     * <tt>YY is number of members, and 
400     * <tt>ZZ</tt> is number of child order sets.
401     */
402    @Override
403    public String toString() {
404        return getGroupFullName() + ":" + size() + "el/" + (size() - childOrderSets.size()) + "m/" + childOrderSets.size() + "ch";
405    }
406
407
408}