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="1") 056 * MemberOrder(sequence="1.1") 057 * MemberOrder(sequence="1.2") 058 * MemberOrder(sequence="1.2.1") 059 * MemberOrder(sequence="1.3") 060 * </pre> 061 * 062 * <p> 063 * With names, creates a hierarchy. 064 * 065 * <pre> 066 * MemberOrder(sequence="1.1") // no parent 067 * MemberOrder(sequence="1.2.1") 068 * MemberOrder(sequence="1.3") 069 * MemberOrder(name="abc", sequence="1") // group is abc, parent is "" 070 * MemberOrder(name="abc", sequence="1.2") 071 * MemberOrder(name="abc,def", sequence="1") // group is def, parent is abc 072 * MemberOrder(name="abc,def", sequence="1.2") 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}