001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.lifecycle.initialize; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.krad.uif.UifConstants; 020import org.kuali.rice.krad.uif.UifPropertyPaths; 021import org.kuali.rice.krad.uif.component.Component; 022import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 023import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleTaskBase; 024import org.kuali.rice.krad.uif.util.LifecycleElement; 025import org.kuali.rice.krad.uif.view.View; 026import org.kuali.rice.krad.uif.view.ViewIndex; 027 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031/** 032 * Assign a unique ID to the component, if one has not already been assigned. 033 * 034 * @author Kuali Rice Team (rice.collab@kuali.org) 035 */ 036public class AssignIdsTask extends ViewLifecycleTaskBase<LifecycleElement> { 037 038 private static final Pattern DIALOGS_PATTERN = Pattern.compile(UifPropertyPaths.DIALOGS + "\\[([0-9]+?)\\]"); 039 040 /** 041 * Create a task to assign component IDs during the initialize phase. 042 */ 043 public AssignIdsTask() { 044 super(LifecycleElement.class); 045 } 046 047 /** 048 * Generate a new ID for a lifecycle element at the current phase. 049 * 050 * <p> 051 * This method used a product of primes similar to the one used for generating String hash 052 * codes. In order to minimize to collisions a large prime is used, then when collisions are 053 * detected a different large prime is used to generate an alternate ID. 054 * </p> 055 * 056 * <p> 057 * The hash code that the generated ID is based on is equivalent (though not identical) to 058 * taking the hash code of the string concenation of all class names, non-null IDs, and 059 * successor index positions in the lifecycle phase tree for all predecessors of the current 060 * phase. This technique leads to a reliably unique ID that is also repeatable across server 061 * instances and test runs. 062 * </p> 063 * 064 * <p> 065 * The use of large primes by this method minimizes collisions, and therefore reduces the 066 * likelihood of a race condition causing components to come out with different IDs on different 067 * server instances and/or test runs. 068 * </p> 069 * 070 * @param element The lifecycle element for which to generate an ID. 071 * @param view View containing the lifecycle element. 072 * @return An ID, unique within the current view, for the given element. 073 * 074 * @see ViewIndex#observeAssignedId(String) 075 * @see String#hashCode() for the algorithm this method is based on. 076 */ 077 public static String generateId(LifecycleElement element, View view) { 078 // Calculate a hash code based on the path to the top of the phase tree 079 // without building a string. 080 int prime = 6971; 081 082 // Initialize hash to the class of the lifecycle element 083 int hash = element.getClass().getName().hashCode(); 084 085 // Add the element's path to the hash code. 086 hash += prime; 087 if (element.getViewPath() != null) { 088 hash += element.getViewPath().hashCode(); 089 } 090 091 // Ensure dialog child components have a unique id (because dialogs can be dynamically requested) 092 // and end up with a similar viewPath 093 // Uses the dialog id as part of the hash for their child component ids 094 if (element.getViewPath() != null && element.getViewPath().startsWith(UifPropertyPaths.DIALOGS + "[")) { 095 Matcher matcher = DIALOGS_PATTERN.matcher(element.getViewPath()); 096 int index = -1; 097 matcher.find(); 098 String strIndex = matcher.group(1); 099 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}