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