View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.lifecycle.initialize;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.krad.uif.UifConstants;
20  import org.kuali.rice.krad.uif.UifPropertyPaths;
21  import org.kuali.rice.krad.uif.component.Component;
22  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
23  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleTaskBase;
24  import org.kuali.rice.krad.uif.util.LifecycleElement;
25  import org.kuali.rice.krad.uif.view.View;
26  import org.kuali.rice.krad.uif.view.ViewIndex;
27  
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  
31  /**
32   * Assign a unique ID to the component, if one has not already been assigned.
33   * 
34   * @author Kuali Rice Team (rice.collab@kuali.org)
35   */
36  public class AssignIdsTask extends ViewLifecycleTaskBase<LifecycleElement> {
37  
38      private static final Pattern DIALOGS_PATTERN = Pattern.compile(UifPropertyPaths.DIALOGS + "\\[([0-9]+?)\\]");
39  
40      /**
41       * Create a task to assign component IDs during the initialize phase.
42       */
43      public AssignIdsTask() {
44          super(LifecycleElement.class);
45      }
46  
47      /**
48       * Generate a new ID for a lifecycle element at the current phase.
49       * 
50       * <p>
51       * This method used a product of primes similar to the one used for generating String hash
52       * codes. In order to minimize to collisions a large prime is used, then when collisions are
53       * detected a different large prime is used to generate an alternate ID.
54       * </p>
55       * 
56       * <p>
57       * The hash code that the generated ID is based on is equivalent (though not identical) to
58       * taking the hash code of the string concenation of all class names, non-null IDs, and
59       * successor index positions in the lifecycle phase tree for all predecessors of the current
60       * phase. This technique leads to a reliably unique ID that is also repeatable across server
61       * instances and test runs.
62       * </p>
63       * 
64       * <p>
65       * The use of large primes by this method minimizes collisions, and therefore reduces the
66       * likelihood of a race condition causing components to come out with different IDs on different
67       * server instances and/or test runs.
68       * </p>
69       * 
70       * @param element The lifecycle element for which to generate an ID.
71       * @param view View containing the lifecycle element.
72       * @return An ID, unique within the current view, for the given element.
73       * 
74       * @see ViewIndex#observeAssignedId(String)
75       * @see String#hashCode() for the algorithm this method is based on.
76       */
77      public static String generateId(LifecycleElement element, View view) {
78          // Calculate a hash code based on the path to the top of the phase tree
79          // without building a string.
80          int prime = 6971;
81  
82          // Initialize hash to the class of the lifecycle element
83          int hash = element.getClass().getName().hashCode();
84          
85          // Add the element's path to the hash code.
86          hash += prime;
87          if (element.getViewPath() != null) {
88              hash += element.getViewPath().hashCode();
89          }
90  
91          // Ensure dialog child components have a unique id (because dialogs can be dynamically requested)
92          // and end up with a similar viewPath
93          // Uses the dialog id as part of the hash for their child component ids
94          if (element.getViewPath() != null && element.getViewPath().startsWith(UifPropertyPaths.DIALOGS + "[")) {
95              Matcher matcher = DIALOGS_PATTERN.matcher(element.getViewPath());
96              int index = -1;
97              matcher.find();
98              String strIndex = matcher.group(1);
99              if (StringUtils.isNotBlank(strIndex)) {
100                 index = Integer.valueOf(strIndex);
101             }
102 
103             if (view.getDialogs() != null && index > -1 && index < view.getDialogs().size()) {
104                 Component parentDialog = view.getDialogs().get(index);
105                 if (parentDialog != null && StringUtils.isNotBlank(parentDialog.getId())) {
106                     hash += parentDialog.getId().hashCode();
107                 }
108             }
109         }
110         
111         // Eliminate negatives without losing precision, and express in base-36
112         String id = Long.toString(((long) hash) - ((long) Integer.MIN_VALUE), 36);
113         while (!view.getViewIndex().observeAssignedId(id)) {
114             // Iteratively take the product of the hash and another large prime
115             // until a unique ID has been generated.
116             hash *= 4507;
117             id = Long.toString(((long) hash) - ((long) Integer.MIN_VALUE), 36);
118         }
119         
120         return UifConstants.COMPONENT_ID_PREFIX + id;
121     }
122 
123     /**
124      * {@inheritDoc}
125      */
126     @Override
127     protected void performLifecycleTask() {
128         LifecycleElement element = getElementState().getElement();
129 
130         if (StringUtils.isBlank(element.getId())) {
131             element.setId(generateId(element, ViewLifecycle.getView()));
132         }
133     }
134 
135 }