001/**
002 * Copyright 2005-2016 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.datadictionary.uif;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.concurrent.ExecutorService;
024import java.util.concurrent.Executors;
025
026import org.apache.commons.lang.StringUtils;
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.kuali.rice.core.api.config.property.ConfigContext;
030import org.kuali.rice.krad.datadictionary.DataDictionaryException;
031import org.kuali.rice.krad.datadictionary.DefaultListableBeanFactory;
032import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
033import org.kuali.rice.krad.uif.UifConstants;
034import org.kuali.rice.krad.uif.UifConstants.ViewType;
035import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
036import org.kuali.rice.krad.uif.service.ViewTypeService;
037import org.kuali.rice.krad.uif.util.CopyUtils;
038import org.kuali.rice.krad.uif.util.ViewModelUtils;
039import org.kuali.rice.krad.uif.view.View;
040import org.kuali.rice.krad.util.KRADConstants;
041import org.springframework.beans.PropertyValues;
042import org.springframework.beans.factory.config.BeanDefinition;
043
044/**
045 * Indexes {@code View} bean entries for retrieval.
046 *
047 * <p>
048 * This is used to retrieve a {@code View} instance by its unique id.
049 * Furthermore, view of certain types (that have a {@code ViewTypeService}
050 * are indexed by their type to support retrieval of views based on parameters.
051 * </p>
052 *
053 * @author Kuali Rice Team (rice.collab@kuali.org)
054 */
055public class UifDictionaryIndex implements Runnable {
056    private static final Log LOG = LogFactory.getLog(UifDictionaryIndex.class);
057    
058    private static final int VIEW_CACHE_SIZE = 1000;
059
060    private DefaultListableBeanFactory ddBeans;
061
062    // view entries keyed by view id with value the spring bean name
063    private Map<String, String> viewBeanEntriesById = new HashMap<String, String>();
064
065    // view entries indexed by type
066    private Map<String, ViewTypeDictionaryIndex> viewEntriesByType = new HashMap<String, ViewTypeDictionaryIndex>();
067
068    // views that are loaded eagerly
069    private Map<String, UifViewPool> viewPools;
070
071    // threadpool size
072    private int threadPoolSize = 4;
073
074    public UifDictionaryIndex(DefaultListableBeanFactory ddBeans) {
075        this.ddBeans = ddBeans;
076    }
077
078    @Override
079    public void run() {
080        try {
081            Integer size = new Integer(ConfigContext.getCurrentContextConfig().getProperty(
082                    KRADConstants.KRAD_DICTIONARY_INDEX_POOL_SIZE));
083            threadPoolSize = size.intValue();
084        } catch (NumberFormatException nfe) {
085            // ignore this, instead the pool will be set to DEFAULT_SIZE
086        }
087
088        buildViewIndicies();
089    }
090
091    /**
092     * Retrieves the View instance with the given id.
093     *
094     * <p>Invokes {@link UifDictionaryIndex#getImmutableViewById(java.lang.String)} to get the view singleton
095     * from spring then returns a copy.</p>
096     *
097     * @param viewId the unique id for the view
098     * @return View instance with the given id
099     * @throws org.kuali.rice.krad.datadictionary.DataDictionaryException if view doesn't exist for id
100     */
101    public View getViewById(final String viewId) {
102        // check for preloaded view
103        if (viewPools.containsKey(viewId)) {
104            final UifViewPool viewPool = viewPools.get(viewId);
105            synchronized (viewPool) {
106                if (!viewPool.isEmpty()) {
107                    View view = viewPool.getViewInstance();
108
109                    // replace view in the pool
110                    Runnable createView = new Runnable() {
111                        public void run() {
112                            View newViewInstance = CopyUtils.copy(getImmutableViewById(viewId));
113                            viewPool.addViewInstance(newViewInstance);
114                        }
115                    };
116
117                    Thread t = new Thread(createView);
118                    t.start();
119
120                    return view;
121                } else {
122                    LOG.info("Pool size for view with id: " + viewId
123                            + " is empty. Considering increasing max pool size.");
124                }
125            }
126        }
127
128        View view = getImmutableViewById(viewId);
129
130        return CopyUtils.copy(view);
131    }
132
133    /**
134     * Retrieves the view singleton from spring that has the given id.
135     *
136     * @param viewId the unique id for the view
137     * @return View instance with the given id
138     */
139    public View getImmutableViewById(String viewId) {
140        String beanName = viewBeanEntriesById.get(viewId);
141        if (StringUtils.isBlank(beanName)) {
142            throw new DataDictionaryException("Unable to find View with id: " + viewId);
143        }
144
145        View view = ddBeans.getBean(beanName, View.class);
146
147        if (UifConstants.ViewStatus.CREATED.equals(view.getViewStatus())) {
148            try {
149                ViewLifecycle.preProcess(view);
150            } catch (IllegalStateException ex) {
151                if (LOG.isDebugEnabled()) {
152                    LOG.debug("preProcess not run due to an IllegalStateException. Exception message: "
153                            + ex.getMessage());
154                }
155            }
156        }
157
158        return view;
159    }
160
161    /**
162     * Retrieves a {@code View} instance that is of the given type based on
163     * the index key
164     *
165     * @param viewTypeName - type name for the view
166     * @param indexKey - Map of index key parameters, these are the parameters the
167     * indexer used to index the view initially and needs to identify
168     * an unique view instance
169     * @return View instance that matches the given index or Null if one is not
170     *         found
171     */
172    public View getViewByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) {
173        String viewId = getViewIdByTypeIndex(viewTypeName, indexKey);
174        if (StringUtils.isNotBlank(viewId)) {
175            return getViewById(viewId);
176        }
177
178        return null;
179    }
180
181    /**
182     * Retrieves the id for the view that is associated with the given view type and index key
183     *
184     * @param viewTypeName type name for the view
185     * @param indexKey Map of index key parameters, these are the parameters the
186     * indexer used to index the view initially and needs to identify an unique view instance
187     * @return id for the view that matches the view type and index or null if a match is not found
188     */
189    public String getViewIdByTypeIndex(ViewType viewTypeName, Map<String, String> indexKey) {
190        String index = buildTypeIndex(indexKey);
191
192        ViewTypeDictionaryIndex typeIndex = getTypeIndex(viewTypeName);
193
194        return typeIndex.get(index);
195    }
196
197    /**
198     * Indicates whether a {@code View} exists for the given view type and index information
199     *
200     * @param viewTypeName - type name for the view
201     * @param indexKey - Map of index key parameters, these are the parameters the indexer used to index
202     * the view initially and needs to identify an unique view instance
203     * @return boolean true if view exists, false if not
204     */
205    public boolean viewByTypeExist(ViewType viewTypeName, Map<String, String> indexKey) {
206        boolean viewExist = false;
207
208        String index = buildTypeIndex(indexKey);
209        ViewTypeDictionaryIndex typeIndex = getTypeIndex(viewTypeName);
210
211        String viewId = typeIndex.get(index);
212        if (StringUtils.isNotBlank(viewId)) {
213            viewExist = true;
214        }
215
216        return viewExist;
217    }
218
219    /**
220     * Retrieves the configured property values for the view bean definition associated with the given id
221     *
222     * <p>
223     * Since constructing the View object can be expensive, when metadata only is needed this method can be used
224     * to retrieve the configured property values. Note this looks at the merged bean definition
225     * </p>
226     *
227     * @param viewId - id for the view to retrieve
228     * @return PropertyValues configured on the view bean definition, or null if view is not found
229     */
230    public PropertyValues getViewPropertiesById(String viewId) {
231        String beanName = viewBeanEntriesById.get(viewId);
232        if (StringUtils.isNotBlank(beanName)) {
233            BeanDefinition beanDefinition = ddBeans.getMergedBeanDefinition(beanName);
234
235            return beanDefinition.getPropertyValues();
236        }
237
238        return null;
239    }
240
241    /**
242     * Retrieves the configured property values for the view bean definition associated with the given type and
243     * index
244     *
245     * <p>
246     * Since constructing the View object can be expensive, when metadata only is needed this method can be used
247     * to retrieve the configured property values. Note this looks at the merged bean definition
248     * </p>
249     *
250     * @param viewTypeName - type name for the view
251     * @param indexKey - Map of index key parameters, these are the parameters the indexer used to index
252     * the view initially and needs to identify an unique view instance
253     * @return PropertyValues configured on the view bean definition, or null if view is not found
254     */
255    public PropertyValues getViewPropertiesByType(ViewType viewTypeName, Map<String, String> indexKey) {
256        String index = buildTypeIndex(indexKey);
257
258        ViewTypeDictionaryIndex typeIndex = getTypeIndex(viewTypeName);
259
260        String beanName = typeIndex.get(index);
261        if (StringUtils.isNotBlank(beanName)) {
262            BeanDefinition beanDefinition = ddBeans.getMergedBeanDefinition(beanName);
263
264            return beanDefinition.getPropertyValues();
265        }
266
267        return null;
268    }
269
270    /**
271     * Gets all {@code View} prototypes configured for the given view type
272     * name
273     *
274     * @param viewTypeName - view type name to retrieve
275     * @return List<View> view prototypes with the given type name, or empty
276     *         list
277     */
278    public List<View> getViewsForType(ViewType viewTypeName) {
279        List<View> typeViews = new ArrayList<View>();
280
281        // get view ids for the type
282        if (viewEntriesByType.containsKey(viewTypeName.name())) {
283            ViewTypeDictionaryIndex typeIndex = viewEntriesByType.get(viewTypeName.name());
284            for (Entry<String, String> typeEntry : typeIndex.getViewIndex().entrySet()) {
285                View typeView = ddBeans.getBean(typeEntry.getValue(), View.class);
286                typeViews.add(typeView);
287            }
288        } else {
289            throw new DataDictionaryException("Unable to find view index for type: " + viewTypeName);
290        }
291
292        return typeViews;
293    }
294
295    /**
296     * Initializes the view index {@code Map} then iterates through all the
297     * beans in the factory that implement {@code View}, adding them to the
298     * index
299     */
300    protected void buildViewIndicies() {
301        LOG.info("Starting View Index Building");
302
303        viewBeanEntriesById = new HashMap<String, String>();
304        viewEntriesByType = new HashMap<String, ViewTypeDictionaryIndex>();
305        viewPools = new HashMap<String, UifViewPool>();
306
307        boolean inDevMode = Boolean.parseBoolean(ConfigContext.getCurrentContextConfig().getProperty(
308                KRADConstants.ConfigParameters.KRAD_DEV_MODE));
309
310        ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);
311
312        String[] beanNames = ddBeans.getBeanNamesForType(View.class);
313        for (final String beanName : beanNames) {
314            BeanDefinition beanDefinition = ddBeans.getMergedBeanDefinition(beanName);
315            PropertyValues propertyValues = beanDefinition.getPropertyValues();
316
317            String id = ViewModelUtils.getStringValFromPVs(propertyValues, "id");
318            if (StringUtils.isBlank(id)) {
319                id = beanName;
320            }
321
322            if (viewBeanEntriesById.containsKey(id)) {
323                throw new DataDictionaryException("Two views must not share the same id. Found duplicate id: " + id);
324            }
325
326            viewBeanEntriesById.put(id, beanName);
327
328            indexViewForType(propertyValues, id);
329
330            // pre-load views if necessary
331            if (!inDevMode) {
332                String poolSizeStr = ViewModelUtils.getStringValFromPVs(propertyValues, "preloadPoolSize");
333                if (StringUtils.isNotBlank(poolSizeStr)) {
334                    int poolSize = Integer.parseInt(poolSizeStr);
335                    if (poolSize < 1) {
336                        continue;
337                    }
338
339                    final View view = (View) ddBeans.getBean(beanName);
340                    final UifViewPool viewPool = new UifViewPool();
341                    viewPool.setMaxSize(poolSize);
342                    for (int j = 0; j < poolSize; j++) {
343                        Runnable createView = new Runnable() {
344                            @Override
345                            public void run() {
346                                viewPool.addViewInstance((View) CopyUtils.copy(view));
347                            }
348                        };
349
350                        executor.execute(createView);
351                    }
352                    viewPools.put(id, viewPool);
353                }
354            }
355        }
356
357        executor.shutdown();
358
359        LOG.info("Completed View Index Building");
360    }
361
362    /**
363     * Performs additional indexing based on the view type associated with the view instance. The
364     * {@code ViewTypeService} associated with the view type name on the instance is invoked to retrieve
365     * the parameter key/value pairs from the configured property values, which are then used to build up an index
366     * used to key the entry
367     *
368     * @param propertyValues - property values configured on the view bean definition
369     * @param id - id (or bean name if id was not set) for the view
370     */
371    protected void indexViewForType(PropertyValues propertyValues, String id) {
372        String viewTypeName = ViewModelUtils.getStringValFromPVs(propertyValues, "viewTypeName");
373        if (StringUtils.isBlank(viewTypeName)) {
374            return;
375        }
376
377        UifConstants.ViewType viewType = ViewType.valueOf(viewTypeName);
378
379        ViewTypeService typeService = KRADServiceLocatorWeb.getViewService().getViewTypeService(viewType);
380        if (typeService == null) {
381            // don't do any further indexing
382            return;
383        }
384
385        // invoke type service to retrieve it parameter name/value pairs
386        Map<String, String> typeParameters = typeService.getParametersFromViewConfiguration(propertyValues);
387
388        // build the index string from the parameters
389        String index = buildTypeIndex(typeParameters);
390
391        // get the index for the type and add the view entry
392        ViewTypeDictionaryIndex typeIndex = getTypeIndex(viewType);
393
394        typeIndex.put(index, id);
395    }
396
397    /**
398     * Retrieves the {@code ViewTypeDictionaryIndex} instance for the given
399     * view type name. If one does not exist yet for the given name, a new
400     * instance is created
401     *
402     * @param viewType - name of the view type to retrieve index for
403     * @return ViewTypeDictionaryIndex instance
404     */
405    protected ViewTypeDictionaryIndex getTypeIndex(UifConstants.ViewType viewType) {
406        ViewTypeDictionaryIndex typeIndex = null;
407
408        if (viewEntriesByType.containsKey(viewType.name())) {
409            typeIndex = viewEntriesByType.get(viewType.name());
410        } else {
411            typeIndex = new ViewTypeDictionaryIndex();
412            viewEntriesByType.put(viewType.name(), typeIndex);
413        }
414
415        return typeIndex;
416    }
417
418    /**
419     * Builds up an index string from the given Map of parameters
420     *
421     * @param typeParameters - Map of parameters to use for index
422     * @return String index
423     */
424    protected String buildTypeIndex(Map<String, String> typeParameters) {
425        String index = "";
426
427        for (String parameterName : typeParameters.keySet()) {
428            if (StringUtils.isNotBlank(index)) {
429                index += "|||";
430            }
431            index += parameterName + "^^" + typeParameters.get(parameterName);
432        }
433
434        return index;
435    }
436
437}