001 /** 002 * Copyright 2005-2013 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 */ 016 package org.kuali.rice.krad.web.bind; 017 018 import org.kuali.rice.krad.uif.view.ViewIndex; 019 import org.kuali.rice.krad.uif.view.ViewModel; 020 import org.springframework.beans.BeanWrapperImpl; 021 import org.springframework.beans.BeansException; 022 import org.springframework.beans.InvalidPropertyException; 023 import org.springframework.beans.NullValueInNestedPathException; 024 import org.springframework.beans.PropertyAccessorUtils; 025 import org.springframework.beans.PropertyValue; 026 import org.springframework.util.StringUtils; 027 028 import java.beans.PropertyDescriptor; 029 import java.beans.PropertyEditor; 030 import java.util.ArrayList; 031 import java.util.HashSet; 032 import java.util.List; 033 import java.util.Set; 034 035 /** 036 * Class is a top level BeanWrapper for a UIF View Model 037 * 038 * <p> 039 * Registers custom property editors configured on the field associated with the property name for which 040 * we are getting or setting a value. In addition determines if the field requires encryption and if so applies 041 * the {@link UifEncryptionPropertyEditorWrapper} 042 * </p> 043 * 044 * @author Kuali Rice Team (rice.collab@kuali.org) 045 */ 046 public class UifViewBeanWrapper extends BeanWrapperImpl { 047 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(UifViewBeanWrapper.class); 048 049 // this is a handle to the target object so we don't have to cast so often 050 private ViewModel model; 051 052 // this stores all properties this wrapper has already checked 053 // with the view so the service isn't called again 054 private Set<String> processedProperties; 055 056 public UifViewBeanWrapper(ViewModel model) { 057 super(model); 058 059 this.model = model; 060 this.processedProperties = new HashSet<String>(); 061 } 062 063 /** 064 * Attempts to find a corresponding data field for the given property name in the current view or previous view, 065 * then if the field has a property editor configured it is registered with the property editor registry to use 066 * for this property 067 * 068 * @param propertyName - name of the property to find field and editor for 069 */ 070 protected void registerEditorFromView(String propertyName) { 071 if (LOG.isDebugEnabled()) { 072 LOG.debug("Attempting to find property editor for property '" + propertyName + "'"); 073 } 074 075 // check if we already processed this property for this BeanWrapper instance 076 if (processedProperties.contains(propertyName)) { 077 return; 078 } 079 080 // when rendering the page, we will use the view that was just built, for post 081 // we need to use the posted view (not the newly initialized view) 082 ViewIndex viewIndex = null; 083 if (model.getView() != null) { 084 viewIndex = model.getView().getViewIndex(); 085 } else if (model.getPostedView() != null) { 086 viewIndex = model.getPostedView().getViewIndex(); 087 } 088 089 // if view index instance not established we cannot determine property editors 090 if (viewIndex == null) { 091 return; 092 } 093 094 PropertyEditor propertyEditor = null; 095 boolean requiresEncryption = false; 096 097 if (viewIndex.getFieldPropertyEditors().containsKey(propertyName)) { 098 propertyEditor = viewIndex.getFieldPropertyEditors().get(propertyName); 099 } else if (viewIndex.getSecureFieldPropertyEditors().containsKey(propertyName)) { 100 propertyEditor = viewIndex.getSecureFieldPropertyEditors().get(propertyName); 101 requiresEncryption = true; 102 } 103 104 if (propertyEditor != null) { 105 if (LOG.isDebugEnabled()) { 106 LOG.debug("Registering custom editor for property path '" + propertyName 107 + "' and property editor class '" + propertyEditor.getClass().getName() + "'"); 108 } 109 110 if (requiresEncryption) { 111 if (LOG.isDebugEnabled()) { 112 LOG.debug("Enabling encryption for custom editor '" + propertyName + 113 "' and property editor class '" + propertyEditor.getClass().getName() + "'"); 114 } 115 this.registerCustomEditor(null, propertyName, new UifEncryptionPropertyEditorWrapper(propertyEditor)); 116 } else { 117 this.registerCustomEditor(null, propertyName, propertyEditor); 118 } 119 } else if (requiresEncryption) { 120 if (LOG.isDebugEnabled()) { 121 LOG.debug("No custom formatter for property path '" + propertyName 122 + "' but property does require encryption"); 123 } 124 125 this.registerCustomEditor(null, propertyName, new UifEncryptionPropertyEditorWrapper( 126 findEditorForPropertyName(propertyName))); 127 } 128 129 processedProperties.add(propertyName); 130 } 131 132 protected PropertyEditor findEditorForPropertyName(String propertyName) { 133 Class<?> clazz = getPropertyType(propertyName); 134 if (LOG.isDebugEnabled()) { 135 LOG.debug("Attempting retrieval of property editor using class '" 136 + clazz 137 + "' and property path '" 138 + propertyName 139 + "'"); 140 } 141 142 PropertyEditor editor = findCustomEditor(clazz, propertyName); 143 if (editor == null) { 144 if (LOG.isDebugEnabled()) { 145 LOG.debug("No custom property editor found using class '" 146 + clazz 147 + "' and property path '" 148 + propertyName 149 + "'. Attempting to find default property editor class."); 150 } 151 editor = getDefaultEditor(clazz); 152 } 153 154 return editor; 155 } 156 157 @Override 158 public Class<?> getPropertyType(String propertyName) throws BeansException { 159 try { 160 PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName); 161 if (pd != null) { 162 return pd.getPropertyType(); 163 } 164 165 // Maybe an indexed/mapped property... 166 Object value = super.getPropertyValue(propertyName); 167 if (value != null) { 168 return value.getClass(); 169 } 170 171 // Check to see if there is a custom editor, 172 // which might give an indication on the desired target type. 173 Class<?> editorType = guessPropertyTypeFromEditors(propertyName); 174 if (editorType != null) { 175 return editorType; 176 } 177 } catch (InvalidPropertyException ex) { 178 // Consider as not determinable. 179 } 180 181 return null; 182 } 183 184 @Override 185 protected BeanWrapperImpl getBeanWrapperForPropertyPath(String propertyPath) { 186 BeanWrapperImpl beanWrapper = super.getBeanWrapperForPropertyPath(propertyPath); 187 188 PropertyTokenHolder tokens = getPropertyNameTokens(propertyPath); 189 String canonicalName = tokens.canonicalName; 190 191 int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(canonicalName); 192 if (pos != -1) { 193 canonicalName = canonicalName.substring(0, pos); 194 } 195 196 copyCustomEditorsTo(beanWrapper, canonicalName); 197 198 return beanWrapper; 199 } 200 201 @Override 202 public Object getPropertyValue(String propertyName) throws BeansException { 203 registerEditorFromView(propertyName); 204 205 Object value = null; 206 try { 207 value = super.getPropertyValue(propertyName); 208 } catch (NullValueInNestedPathException e) { 209 // swallow null values in path and return null as the value 210 } 211 212 return value; 213 } 214 215 @Override 216 public void setPropertyValue(PropertyValue pv) throws BeansException { 217 registerEditorFromView(pv.getName()); 218 super.setPropertyValue(pv); 219 } 220 221 @Override 222 public void setPropertyValue(String propertyName, Object value) throws BeansException { 223 registerEditorFromView(propertyName); 224 super.setPropertyValue(propertyName, value); 225 } 226 227 @Override 228 public void setWrappedInstance(Object object, String nestedPath, Object rootObject) { 229 //TODO clear cache? 230 model = (ViewModel) object; 231 super.setWrappedInstance(object, nestedPath, rootObject); 232 } 233 234 @Override 235 public void setWrappedInstance(Object object) { 236 //TODO clear cache? 237 model = (ViewModel) object; 238 super.setWrappedInstance(object); 239 } 240 241 /** 242 * Parse the given property name into the corresponding property name tokens. 243 * 244 * @param propertyName the property name to parse 245 * @return representation of the parsed property tokens 246 */ 247 private PropertyTokenHolder getPropertyNameTokens(String propertyName) { 248 PropertyTokenHolder tokens = new PropertyTokenHolder(); 249 String actualName = null; 250 List<String> keys = new ArrayList<String>(2); 251 int searchIndex = 0; 252 while (searchIndex != -1) { 253 int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); 254 searchIndex = -1; 255 if (keyStart != -1) { 256 int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length()); 257 if (keyEnd != -1) { 258 if (actualName == null) { 259 actualName = propertyName.substring(0, keyStart); 260 } 261 String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); 262 if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { 263 key = key.substring(1, key.length() - 1); 264 } 265 keys.add(key); 266 searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); 267 } 268 } 269 } 270 tokens.actualName = (actualName != null ? actualName : propertyName); 271 tokens.canonicalName = tokens.actualName; 272 if (!keys.isEmpty()) { 273 tokens.canonicalName += PROPERTY_KEY_PREFIX + 274 StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + 275 PROPERTY_KEY_SUFFIX; 276 tokens.keys = StringUtils.toStringArray(keys); 277 } 278 return tokens; 279 } 280 281 private static class PropertyTokenHolder { 282 283 public String canonicalName; 284 285 public String actualName; 286 287 public String[] keys; 288 } 289 }