/* * Copyright 2017 OmniFaces * * 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 org.omnifaces.cdi.param; import static java.lang.Boolean.parseBoolean; import static java.util.Arrays.asList; import static javax.faces.validator.BeanValidator.DISABLE_DEFAULT_BEAN_VALIDATOR_PARAM_NAME; import static org.omnifaces.util.Beans.getQualifier; import static org.omnifaces.util.Components.setLabel; import static org.omnifaces.util.Faces.createConverter; import static org.omnifaces.util.Faces.createValidator; import static org.omnifaces.util.Faces.evaluateExpressionGet; import static org.omnifaces.util.Faces.getApplication; import static org.omnifaces.util.Faces.getInitParameter; import static org.omnifaces.util.FacesLocal.getMessageBundle; import static org.omnifaces.util.FacesLocal.getRequestParameterValues; import static org.omnifaces.util.FacesLocal.getRequestPathInfo; import static org.omnifaces.util.Messages.createError; import static org.omnifaces.util.Platform.getBeanValidator; import static org.omnifaces.util.Platform.isBeanValidationAvailable; import static org.omnifaces.util.Reflection.setPropertiesWithCoercion; import static org.omnifaces.util.Utils.coalesce; import static org.omnifaces.util.Utils.containsByClassName; import static org.omnifaces.util.Utils.getDefaultValue; import static org.omnifaces.util.Utils.isEmpty; import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.ResourceBundle; import java.util.Set; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import javax.enterprise.inject.spi.InjectionPoint; import javax.faces.application.Application; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; import javax.faces.convert.ConverterException; import javax.faces.validator.BeanValidator; import javax.faces.validator.RequiredValidator; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; import javax.inject.Inject; import javax.validation.ConstraintViolation; import org.omnifaces.cdi.Param; /** * Producer for a request or path parameter as defined by the <code>@</code>{@link Param} annotation. * * @since 1.6 * @author Arjan Tijms */ @Dependent public class ParamProducer { private static final String DEFAULT_REQUIRED_MESSAGE = "{0}: Value is required"; private static volatile Boolean interpretEmptyStringSubmittedValuesAsNull; @SuppressWarnings("unused") // Workaround for OpenWebBeans not properly passing it as produce() method argument. @Inject private InjectionPoint injectionPoint; @Produces @Param public <V> ParamValue<V> produce(InjectionPoint injectionPoint) { Param param = getQualifier(injectionPoint, Param.class); String name = getName(param, injectionPoint); int pathIndex = param.pathIndex(); String label = getLabel(param, injectionPoint); Type type = injectionPoint.getType(); if (type instanceof ParameterizedType && ParamValue.class.isAssignableFrom((Class<?>) ((ParameterizedType) type).getRawType())) { type = ((ParameterizedType) type).getActualTypeArguments()[0]; } FacesContext context = FacesContext.getCurrentInstance(); String[] submittedValues = pathIndex > -1 ? getPathParameter(context, pathIndex) : getRequestParameterValues(context, name); Object[] convertedValues = getConvertedValues(context, param, label, submittedValues, type); V paramValue = coerceValues(type, convertedValues); if (!validateValues(context, param, label, submittedValues, convertedValues, paramValue, injectionPoint)) { paramValue = null; } return new ParamValue<>(submittedValues, param, type, paramValue); } private static String[] getPathParameter(FacesContext context, int pathIndex) { String pathInfo = getRequestPathInfo(context); if (pathInfo != null) { String[] pathParts = pathInfo.substring(1).split("/"); if (pathIndex < pathParts.length) { return new String[] { pathParts[pathIndex] }; } } return null; } static Object[] getConvertedValues(FacesContext context, Param param, String label, String[] submittedValues, Type type) { if (submittedValues == null) { return null; } Object[] convertedValues = new Object[submittedValues.length]; UIComponent component = context.getViewRoot(); Object originalLabel = component.getAttributes().get("label"); boolean valid = true; try { setLabel(component, label); Converter converter = getConverter(param, getTargetType(type)); for (int i = 0; i < submittedValues.length; i++) { String submittedValue = submittedValues[i]; if (submittedValue != null && interpretEmptyStringSubmittedValuesAsNull(context) && submittedValue.isEmpty()) { submittedValue = null; submittedValues[i] = null; } try { convertedValues[i] = (converter != null) ? converter.getAsObject(context, component, submittedValue) : submittedValue; } catch (ConverterException e) { valid = false; addConverterMessage(context, component, label, submittedValue, e, getConverterMessage(param)); } } } finally { setLabel(component, originalLabel); } if (!valid) { context.validationFailed(); return null; } return convertedValues; } private static boolean interpretEmptyStringSubmittedValuesAsNull(FacesContext context) { if (interpretEmptyStringSubmittedValuesAsNull != null) { return interpretEmptyStringSubmittedValuesAsNull; } interpretEmptyStringSubmittedValuesAsNull = parseBoolean(context.getExternalContext() .getInitParameter("javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL")); return interpretEmptyStringSubmittedValuesAsNull; } @SuppressWarnings("unchecked") static <V> V coerceValues(Type type, Object... values) { if (type instanceof ParameterizedType) { return coerceValues(((ParameterizedType) type).getRawType(), values); } if (!(type instanceof Class)) { return null; } Class<?> cls = (Class<?>) type; Object coercedValue = null; if (values != null) { if (cls.isArray()) { coercedValue = Array.newInstance(cls.getComponentType(), values.length); for (int i = 0; i < values.length; i++) { Array.set(coercedValue, i, coerceValues(cls.getComponentType(), values[i])); } } else if (List.class.isAssignableFrom(cls)) { coercedValue = asList(values); } else { coercedValue = values.length == 0 ? null : values[0]; } } if (coercedValue == null) { coercedValue = getDefaultValue(cls); } return (V) coercedValue; } private static <V> boolean validateValues(FacesContext context, Param param, String label, String[] submittedValues, Object[] convertedValues, V paramValue, InjectionPoint injectionPoint) { boolean valid = true; UIComponent component = context.getViewRoot(); Object originalLabel = component.getAttributes().get("label"); try { setLabel(component, label); valid = validateRequired(context, param, label, convertedValues); if (valid) { valid = validateBean(context, param, label, paramValue, injectionPoint); } if (valid && convertedValues != null) { valid = validateFaces(context, param, label, convertedValues, submittedValues); } } finally { setLabel(component, originalLabel); } if (!valid) { context.validationFailed(); } return valid; } private static boolean validateRequired(FacesContext context, Param param, String label, Object[] convertedValues) { if (param.required() && isEmpty(convertedValues)) { addRequiredMessage(context, context.getViewRoot(), label, getRequiredMessage(param)); return false; } return true; } private static <V> boolean validateBean(FacesContext context, Param param, String label, V paramValue, InjectionPoint injectionPoint) { if (shouldDoBeanValidation(param)) { Set<ConstraintViolation<?>> violations = doBeanValidation(paramValue, injectionPoint); if (!violations.isEmpty()) { for (ConstraintViolation<?> violation : violations) { context.addMessage(context.getViewRoot().getClientId(context), createError(violation.getMessage(), label)); } return false; } } return true; } private static boolean validateFaces(FacesContext context, Param param, String label, Object[] convertedValues, String[] submittedValues) { boolean valid = true; for (Validator validator : getValidators(param)) { int i = 0; for (Object convertedValue : convertedValues) { try { validator.validate(context, context.getViewRoot(), convertedValue); } catch (ValidatorException e) { addValidatorMessages(context, context.getViewRoot(), label, submittedValues[i], e, getValidatorMessage(param)); valid = false; } i++; } } return valid; } private static Converter getConverter(Param requestParameter, Class<?> targetType) { Class<?> classIdentifier = requestParameter.converterClass() == Converter.class ? targetType : requestParameter.converterClass(); Converter converter = createConverter(coalesce(evaluateExpressionGet(requestParameter.converter()), classIdentifier)); if (converter != null) { setPropertiesWithCoercion(converter, getConverterAttributes(requestParameter)); } return converter; } @SuppressWarnings("unchecked") private static <V> Class<V> getTargetType(Type type) { if (type instanceof Class && ((Class<?>) type).isArray()) { return (Class<V>) ((Class<?>) type).getComponentType(); } else if (type instanceof ParameterizedType) { return (Class<V>) ((ParameterizedType) type).getActualTypeArguments()[0]; } else { return (Class<V>) type; } } private static String getName(Param requestParameter, InjectionPoint injectionPoint) { String name = requestParameter.name(); if (isEmpty(name)) { name = injectionPoint.getMember().getName(); } else { name = evaluateExpressionAsString(name); } return name; } private static String getLabel(Param requestParameter, InjectionPoint injectionPoint) { String label = requestParameter.label(); if (isEmpty(label)) { label = getName(requestParameter, injectionPoint); } else { label = evaluateExpressionAsString(label); } return label; } private static String getValidatorMessage(Param requestParameter) { return evaluateExpressionAsString(requestParameter.validatorMessage()); } private static String getConverterMessage(Param requestParameter) { return evaluateExpressionAsString(requestParameter.converterMessage()); } private static String getRequiredMessage(Param requestParameter) { return evaluateExpressionAsString(requestParameter.requiredMessage()); } private static String evaluateExpressionAsString(String expression) { if (isEmpty(expression)) { return expression; } Object expressionResult = evaluateExpressionGet(expression); if (expressionResult == null) { return null; } return expressionResult.toString(); } private static boolean shouldDoBeanValidation(Param requestParameter) { // If bean validation is explicitly disabled for this instance, immediately return false if (requestParameter.disableBeanValidation()) { return false; } // Next check if bean validation has been disabled globally, but only if this hasn't been overridden locally if (!requestParameter.overrideGlobalBeanValidationDisabled() && parseBoolean(getInitParameter(DISABLE_DEFAULT_BEAN_VALIDATOR_PARAM_NAME))) { return false; } // For all other cases, the availability of bean validation determines if we attempt bean validation or not. return isBeanValidationAvailable(); } @SuppressWarnings({ "rawtypes", "unchecked" }) private static <V> Set<ConstraintViolation<?>> doBeanValidation(V paramValue, InjectionPoint injectionPoint) { Class<?> base = injectionPoint.getBean().getBeanClass(); String property = injectionPoint.getMember().getName(); Type type = injectionPoint.getType(); // Check if the target property in which we are injecting in our special holder/wrapper type // ParamValue or not. If it's the latter, pre-wrap our value (otherwise types for bean validation // would not match) Object valueToValidate = paramValue; if (type instanceof ParameterizedType) { Type propertyRawType = ((ParameterizedType) type).getRawType(); if (propertyRawType.equals(ParamValue.class)) { valueToValidate = new ParamValue<>(null, null, null, paramValue); } } return (Set) getBeanValidator().validateValue(base, property, valueToValidate); } private static List<Validator> getValidators(Param requestParameter) { List<Validator> validators = new ArrayList<>(); for (String validatorIdentifier : requestParameter.validators()) { Validator validator = createValidator(evaluateExpressionGet(validatorIdentifier)); if (validator != null) { validators.add(validator); } } for (Class<? extends Validator> validatorClass : requestParameter.validatorClasses()) { Validator validator = createValidator(validatorClass); if (validator != null) { validators.add(validator); } } // Process the default validators Application application = getApplication(); for (Entry<String, String> validatorEntry : application.getDefaultValidatorInfo().entrySet()) { String validatorID = validatorEntry.getKey(); String validatorClassName = validatorEntry.getValue(); // Check that the validator ID is not the BeanValidator one which we handle in a special way. // And make sure the default validator is not already set manually as well. if (!validatorID.equals(BeanValidator.VALIDATOR_ID) && !containsByClassName(validators, validatorClassName)) { validators.add(application.createValidator(validatorID)); } } // Set the attributes on all instantiated validators. We don't distinguish here // which attribute should go to which validator. Map<String, Object> validatorAttributes = getValidatorAttributes(requestParameter); for (Validator validator : validators) { setPropertiesWithCoercion(validator, validatorAttributes); } return validators; } private static Map<String, Object> getConverterAttributes(Param requestParameter) { Map<String, Object> attributeMap = new HashMap<>(); Attribute[] attributes = requestParameter.converterAttributes(); for (Attribute attribute : attributes) { attributeMap.put(attribute.name(), evaluateExpressionGet(attribute.value())); } return attributeMap; } private static Map<String, Object> getValidatorAttributes(Param requestParameter) { Map<String, Object> attributeMap = new HashMap<>(); Attribute[] attributes = requestParameter.validatorAttributes(); for (Attribute attribute : attributes) { attributeMap.put(attribute.name(), evaluateExpressionGet(attribute.value())); } return attributeMap; } private static void addConverterMessage(FacesContext context, UIComponent component, String label, String submittedValue, ConverterException ce, String converterMessage) { FacesMessage message; if (!isEmpty(converterMessage)) { message = createError(converterMessage, submittedValue, label); } else { message = ce.getFacesMessage(); if (message == null) { // If the converter didn't add a FacesMessage, set a generic one. message = createError("Conversion failed for {0} because: {1}", submittedValue, ce.getMessage()); } } context.addMessage(component.getClientId(context), message); } private static void addRequiredMessage(FacesContext context, UIComponent component, String label, String requiredMessage) { FacesMessage message = null; if (!isEmpty(requiredMessage)) { message = createError(requiredMessage, null, label); } else { // (Ab)use RequiredValidator to get the same message that all required attributes are using. try { new RequiredValidator().validate(context, component, null); } catch (ValidatorException ve) { message = ve.getFacesMessage(); } if (message == null) { // RequiredValidator didn't throw or its exception did not have a message set. ResourceBundle messageBundle = getMessageBundle(context); String defaultRequiredMessage = (messageBundle != null) ? messageBundle.getString(UIInput.REQUIRED_MESSAGE_ID) : null; message = createError(coalesce(defaultRequiredMessage, requiredMessage, DEFAULT_REQUIRED_MESSAGE), label); } } context.addMessage(component.getClientId(context), message); } private static void addValidatorMessages(FacesContext context, UIComponent component, String label, String submittedValue, ValidatorException ve, String validatorMessage) { String clientId = component.getClientId(context); if (!isEmpty(validatorMessage)) { context.addMessage(clientId, createError(validatorMessage, submittedValue, label)); } else { for (FacesMessage facesMessage : getFacesMessages(ve)) { context.addMessage(clientId, facesMessage); } } } private static List<FacesMessage> getFacesMessages(ValidatorException ve) { List<FacesMessage> facesMessages = new ArrayList<>(); if (ve.getFacesMessages() != null) { facesMessages.addAll(ve.getFacesMessages()); } else if (ve.getFacesMessage() != null) { facesMessages.add(ve.getFacesMessage()); } return facesMessages; } }