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 }