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}