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 }