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    }