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}