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 }