/* * Copyright 2006-2007 Graeme Rocher * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package grails.web.databinding; import grails.databinding.CollectionDataBindingSource; import grails.databinding.DataBinder; import grails.databinding.DataBindingSource; import grails.util.Environment; import grails.util.Holders; import grails.validation.ValidationErrors; import groovy.lang.GroovySystem; import groovy.lang.MetaClass; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.ServletRequest; import grails.core.GrailsApplication; import grails.core.GrailsDomainClass; import grails.core.GrailsDomainClassProperty; import grails.web.mime.MimeType; import grails.web.mime.MimeTypeResolver; import grails.web.mime.MimeTypeUtils; import org.grails.core.exceptions.GrailsConfigurationException; import org.grails.datastore.mapping.model.PersistentEntity; import org.grails.datastore.mapping.model.PersistentProperty; import org.grails.datastore.mapping.model.types.OneToOne; import org.grails.web.databinding.DefaultASTDatabindingHelper; import org.grails.web.databinding.bindingsource.DataBindingSourceRegistry; import org.grails.web.databinding.bindingsource.DefaultDataBindingSourceRegistry; import org.grails.web.databinding.bindingsource.InvalidRequestBodyException; import org.springframework.context.ApplicationContext; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; /** * Utility methods to perform data binding from Grails objects. * * @author Graeme Rocher * @since 1.0 */ @SuppressWarnings("rawtypes") public class DataBindingUtils { public static final String DATA_BINDER_BEAN_NAME = "grailsWebDataBinder"; private static final String BLANK = ""; private static final Map<Class, List> CLASS_TO_BINDING_INCLUDE_LIST = new ConcurrentHashMap<Class, List>(); /** * Associations both sides of any bidirectional relationships found in the object and source map to bind * * @param object The object * @param source The source map * @param domainClass The DomainClass for the object * @deprecated Use {@link #assignBidirectionalAssociations(Object, Map, PersistentEntity)} instead */ @Deprecated public static void assignBidirectionalAssociations(Object object, Map source, GrailsDomainClass domainClass) { if (source == null) { return; } for (Object key : source.keySet()) { String propertyName = key.toString(); if (propertyName.indexOf('.') > -1) { propertyName = propertyName.substring(0, propertyName.indexOf('.')); } if (domainClass.hasPersistentProperty(propertyName)) { GrailsDomainClassProperty prop = domainClass.getPropertyByName(propertyName); if (prop != null && prop.isOneToOne() && prop.isBidirectional()) { Object val = source.get(key); GrailsDomainClassProperty otherSide = prop.getOtherSide(); if (val != null && otherSide != null) { MetaClass mc = GroovySystem.getMetaClassRegistry().getMetaClass(val.getClass()); try { mc.setProperty(val, otherSide.getName(), object); } catch (Exception e) { // ignore } } } } } } /** * Associations both sides of any bidirectional relationships found in the object and source map to bind * * @param object The object * @param source The source map * @param persistentEntity The PersistentEntity for the object */ public static void assignBidirectionalAssociations(Object object, Map source, PersistentEntity persistentEntity) { if (source == null) { return; } for (Object key : source.keySet()) { String propertyName = key.toString(); if (propertyName.indexOf('.') > -1) { propertyName = propertyName.substring(0, propertyName.indexOf('.')); } PersistentProperty prop = persistentEntity.getPropertyByName(propertyName); if (prop != null && prop instanceof OneToOne && ((OneToOne) prop).isBidirectional()) { Object val = source.get(key); PersistentProperty otherSide = ((OneToOne) prop).getInverseSide(); if (val != null && otherSide != null) { MetaClass mc = GroovySystem.getMetaClassRegistry().getMetaClass(val.getClass()); try { mc.setProperty(val, otherSide.getName(), object); } catch (Exception e) { // ignore } } } } } /** * Binds the given source object to the given target object performing type conversion if necessary * * @param object The object to bind to * @param source The source object * @return A BindingResult if there were errors or null if it was successful */ public static BindingResult bindObjectToInstance(Object object, Object source) { return bindObjectToInstance(object, source, getBindingIncludeList(object), Collections.emptyList(), null); } private static List getBindingIncludeList(final Object object) { List includeList = Collections.emptyList(); try { final Class<? extends Object> objectClass = object.getClass(); if (CLASS_TO_BINDING_INCLUDE_LIST.containsKey(objectClass)) { includeList = CLASS_TO_BINDING_INCLUDE_LIST.get(objectClass); } else { final Field whiteListField = objectClass.getDeclaredField(DefaultASTDatabindingHelper.DEFAULT_DATABINDING_WHITELIST); if (whiteListField != null) { if ((whiteListField.getModifiers() & Modifier.STATIC) != 0) { final Object whiteListValue = whiteListField.get(objectClass); if (whiteListValue instanceof List) { includeList = (List)whiteListValue; } } } if (!Environment.getCurrent().isReloadEnabled()) { CLASS_TO_BINDING_INCLUDE_LIST.put(objectClass, includeList); } } } catch (Exception e) { } return includeList; } /** * Binds the given source object to the given target object performing type conversion if necessary * * @param domain The GrailsDomainClass instance * @param object The object to bind to * @param source The source object * * @see grails.core.GrailsDomainClass * * @return A BindingResult if there were errors or null if it was successful * * @deprecated Use {@link #bindObjectToDomainInstance(PersistentEntity, Object, Object)} instead */ @Deprecated public static BindingResult bindObjectToDomainInstance(GrailsDomainClass domain, Object object, Object source) { return bindObjectToDomainInstance(domain, object, source, getBindingIncludeList(object), Collections.emptyList(), null); } /** * Binds the given source object to the given target object performing type conversion if necessary * * @param entity The PersistentEntity instance * @param object The object to bind to * @param source The source object * * @see org.grails.datastore.mapping.model.PersistentEntity * * @return A BindingResult if there were errors or null if it was successful */ public static BindingResult bindObjectToDomainInstance(PersistentEntity entity, Object object, Object source) { return bindObjectToDomainInstance(entity, object, source, getBindingIncludeList(object), Collections.emptyList(), null); } /** * For each DataBindingSource provided by collectionBindingSource a new instance of targetType is created, * data binding is imposed on that instance with the DataBindingSource and the instance is added to the end of * collectionToPopulate * * @param targetType The type of objects to create, must be a concrete class * @param collectionToPopulate A collection to populate with new instances of targetType * @param collectionBindingSource A CollectionDataBindingSource * @since 2.3 */ public static <T> void bindToCollection(final Class<T> targetType, final Collection<T> collectionToPopulate, final CollectionDataBindingSource collectionBindingSource) throws InstantiationException, IllegalAccessException { final GrailsApplication application = Holders.findApplication(); PersistentEntity entity = null; if (application != null) { try { entity = application.getMappingContext().getPersistentEntity(targetType.getClass().getName()); } catch (GrailsConfigurationException e) { //no-op } } final List<DataBindingSource> dataBindingSources = collectionBindingSource.getDataBindingSources(); for(final DataBindingSource dataBindingSource : dataBindingSources) { final T newObject = targetType.newInstance(); bindObjectToDomainInstance(entity, newObject, dataBindingSource, getBindingIncludeList(newObject), Collections.emptyList(), null); collectionToPopulate.add(newObject); } } public static <T> void bindToCollection(final Class<T> targetType, final Collection<T> collectionToPopulate, final ServletRequest request) throws InstantiationException, IllegalAccessException { final GrailsApplication grailsApplication = Holders.findApplication(); final CollectionDataBindingSource collectionDataBindingSource = createCollectionDataBindingSource(grailsApplication, targetType, request); bindToCollection(targetType, collectionToPopulate, collectionDataBindingSource); } /** * Binds the given source object to the given target object performing type conversion if necessary * * @param object The object to bind to * @param source The source object * @param include The list of properties to include * @param exclude The list of properties to exclude * @param filter The prefix to filter by * * @return A BindingResult if there were errors or null if it was successful */ public static BindingResult bindObjectToInstance(Object object, Object source, List include, List exclude, String filter) { if (include == null && exclude == null) { include = getBindingIncludeList(object); } GrailsApplication application = Holders.findApplication(); PersistentEntity entity = null; if (application != null) { try { entity = application.getMappingContext().getPersistentEntity(object.getClass().getName()); } catch (GrailsConfigurationException e) { //no-op } } return bindObjectToDomainInstance(entity, object, source, include, exclude, filter); } /** * Binds the given source object to the given target object performing type conversion if necessary * * @param domain The GrailsDomainClass instance * @param object The object to bind to * @param source The source object * @param include The list of properties to include * @param exclude The list of properties to exclude * @param filter The prefix to filter by * * @see grails.core.GrailsDomainClass * * @return A BindingResult if there were errors or null if it was successful * * @deprecated Use {@link #bindObjectToDomainInstance(PersistentEntity, Object, Object, List, List, String)} instead */ @SuppressWarnings("unchecked") @Deprecated public static BindingResult bindObjectToDomainInstance(GrailsDomainClass domain, Object object, Object source, List include, List exclude, String filter) { BindingResult bindingResult = null; GrailsApplication grailsApplication = null; if (domain != null) { grailsApplication = domain.getApplication(); } if (grailsApplication == null) { grailsApplication = Holders.findApplication(); } try { final DataBindingSource bindingSource = createDataBindingSource(grailsApplication, object.getClass(), source); final DataBinder grailsWebDataBinder = getGrailsWebDataBinder(grailsApplication); grailsWebDataBinder.bind(object, bindingSource, filter, include, exclude); } catch (InvalidRequestBodyException e) { String messageCode = "invalidRequestBody"; Class objectType = object.getClass(); String defaultMessage = "An error occurred parsing the body of the request"; String[] codes = getMessageCodes(messageCode, objectType); bindingResult = new BeanPropertyBindingResult(object, objectType.getName()); bindingResult.addError(new ObjectError(bindingResult.getObjectName(), codes, null, defaultMessage)); } catch (Exception e) { bindingResult = new BeanPropertyBindingResult(object, object.getClass().getName()); bindingResult.addError(new ObjectError(bindingResult.getObjectName(), e.getMessage())); } if (domain != null && bindingResult != null) { BindingResult newResult = new ValidationErrors(object); for (Object error : bindingResult.getAllErrors()) { if (error instanceof FieldError) { FieldError fieldError = (FieldError)error; final boolean isBlank = BLANK.equals(fieldError.getRejectedValue()); if (!isBlank) { newResult.addError(fieldError); } else if (domain.hasPersistentProperty(fieldError.getField())) { final boolean isOptional = domain.getPropertyByName(fieldError.getField()).isOptional(); if (!isOptional) { newResult.addError(fieldError); } } else { newResult.addError(fieldError); } } else { newResult.addError((ObjectError)error); } } bindingResult = newResult; } MetaClass mc = GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass()); if (mc.hasProperty(object, "errors")!=null && bindingResult!=null) { ValidationErrors errors = new ValidationErrors(object); errors.addAllErrors(bindingResult); mc.setProperty(object,"errors", errors); } return bindingResult; } /** * Binds the given source object to the given target object performing type conversion if necessary * * @param entity The PersistentEntity instance * @param object The object to bind to * @param source The source object * @param include The list of properties to include * @param exclude The list of properties to exclude * @param filter The prefix to filter by * * @see org.grails.datastore.mapping.model.PersistentEntity * * @return A BindingResult if there were errors or null if it was successful */ @SuppressWarnings("unchecked") public static BindingResult bindObjectToDomainInstance(PersistentEntity entity, Object object, Object source, List include, List exclude, String filter) { BindingResult bindingResult = null; GrailsApplication grailsApplication = Holders.findApplication(); try { final DataBindingSource bindingSource = createDataBindingSource(grailsApplication, object.getClass(), source); final DataBinder grailsWebDataBinder = getGrailsWebDataBinder(grailsApplication); grailsWebDataBinder.bind(object, bindingSource, filter, include, exclude); } catch (InvalidRequestBodyException e) { String messageCode = "invalidRequestBody"; Class objectType = object.getClass(); String defaultMessage = "An error occurred parsing the body of the request"; String[] codes = getMessageCodes(messageCode, objectType); bindingResult = new BeanPropertyBindingResult(object, objectType.getName()); bindingResult.addError(new ObjectError(bindingResult.getObjectName(), codes, null, defaultMessage)); } catch (Exception e) { bindingResult = new BeanPropertyBindingResult(object, object.getClass().getName()); bindingResult.addError(new ObjectError(bindingResult.getObjectName(), e.getMessage())); } if (entity != null && bindingResult != null) { BindingResult newResult = new ValidationErrors(object); for (Object error : bindingResult.getAllErrors()) { if (error instanceof FieldError) { FieldError fieldError = (FieldError)error; final boolean isBlank = BLANK.equals(fieldError.getRejectedValue()); if (!isBlank) { newResult.addError(fieldError); } else { PersistentProperty property = entity.getPropertyByName(fieldError.getField()); if (property != null) { final boolean isOptional = property.isNullable(); if (!isOptional) { newResult.addError(fieldError); } } else { newResult.addError(fieldError); } } } else { newResult.addError((ObjectError)error); } } bindingResult = newResult; } MetaClass mc = GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass()); if (mc.hasProperty(object, "errors")!=null && bindingResult!=null) { ValidationErrors errors = new ValidationErrors(object); errors.addAllErrors(bindingResult); mc.setProperty(object,"errors", errors); } return bindingResult; } protected static String[] getMessageCodes(String messageCode, Class objectType) { String[] codes = {objectType.getName() + "." + messageCode, messageCode}; return codes; } public static DataBindingSourceRegistry getDataBindingSourceRegistry(GrailsApplication grailsApplication) { DataBindingSourceRegistry registry = null; if(grailsApplication != null) { ApplicationContext context = grailsApplication.getMainContext(); if(context != null) { if(context.containsBean(DataBindingSourceRegistry.BEAN_NAME)) { registry = context.getBean(DataBindingSourceRegistry.BEAN_NAME, DataBindingSourceRegistry.class); } } } if(registry == null) { registry = new DefaultDataBindingSourceRegistry(); } return registry; } public static DataBindingSource createDataBindingSource(GrailsApplication grailsApplication, Class bindingTargetType, Object bindingSource) { final DataBindingSourceRegistry registry = getDataBindingSourceRegistry(grailsApplication); final MimeType mimeType = getMimeType(grailsApplication, bindingSource); return registry.createDataBindingSource(mimeType, bindingTargetType, bindingSource); } public static CollectionDataBindingSource createCollectionDataBindingSource(GrailsApplication grailsApplication, Class bindingTargetType, Object bindingSource) { final DataBindingSourceRegistry registry = getDataBindingSourceRegistry(grailsApplication); final MimeType mimeType = getMimeType(grailsApplication, bindingSource); return registry.createCollectionDataBindingSource(mimeType, bindingTargetType, bindingSource); } public static MimeType getMimeType(GrailsApplication grailsApplication, Object bindingSource) { final MimeTypeResolver mimeTypeResolver = getMimeTypeResolver(grailsApplication); return resolveMimeType(bindingSource, mimeTypeResolver); } public static MimeTypeResolver getMimeTypeResolver( GrailsApplication grailsApplication) { MimeTypeResolver mimeTypeResolver = null; if(grailsApplication != null) { ApplicationContext context = grailsApplication.getMainContext(); if(context != null) { if(context.containsBean(MimeTypeResolver.BEAN_NAME)) { mimeTypeResolver = context.getBean(MimeTypeResolver.BEAN_NAME, MimeTypeResolver.class); } } } return mimeTypeResolver; } public static MimeType resolveMimeType(Object bindingSource, MimeTypeResolver mimeTypeResolver) { return MimeTypeUtils.resolveMimeType(bindingSource, mimeTypeResolver); } private static DataBinder getGrailsWebDataBinder(final GrailsApplication grailsApplication) { DataBinder dataBinder = null; if (grailsApplication != null) { final ApplicationContext mainContext = grailsApplication.getMainContext(); if (mainContext != null && mainContext.containsBean(DATA_BINDER_BEAN_NAME)) { dataBinder = mainContext.getBean(DATA_BINDER_BEAN_NAME, DataBinder.class); } } if (dataBinder == null) { // this should really never happen in the running app as the binder // should always be found in the context dataBinder = new GrailsWebDataBinder(grailsApplication); } return dataBinder; } @SuppressWarnings("unchecked") public static Map convertPotentialGStrings(Map<Object, Object> args) { Map newArgs = new HashMap(args.size()); for (Map.Entry<Object, Object> entry : args.entrySet()) { newArgs.put(unwrapGString(entry.getKey()), unwrapGString(entry.getValue())); } return newArgs; } private static Object unwrapGString(Object value) { if (value instanceof CharSequence) { return value.toString(); } return value; } }