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}