/*
* The MIT License
*
* Copyright 2015 nikku.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package support.form;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.core.MultivaluedMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A parser that converts {@link MultivaluedMap<String, String>} to form objects.
*
* @author nikku
*/
public class FormParser {
private static final Logger LOG = LoggerFactory.getLogger("support.form.parser");
public static <T> T toForm(Class<T> type, MultivaluedMap<String, String> bodyMap) {
T instance = createInstance(type);
bodyMap.keySet().forEach(key -> {
populateField(type, instance, key, bodyMap.get(key));
});
return instance;
}
public static <T> T createInstance(Class<T> type) {
LOG.debug("Instantiate {}", type.getName());
try {
return type.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
LOG.error(
"Failed to instantiate {}. " +
"Does it define an accessible default constructor?", e);
throw new InternalServerErrorException(e);
}
}
static void populateField(Class<?> type, Object instance, String key, List<String> values) {
LOG.debug("Populate {}#{} with values {}", type.getName(), key, values);
String propertyName = key;
String nestedPropertyName = null;
boolean collectionProperty = false;
int nestedSeparatorIdx = key.indexOf(".");
if (nestedSeparatorIdx != -1) {
propertyName = key.substring(0, nestedSeparatorIdx);
nestedPropertyName = key.substring(nestedSeparatorIdx + 1);
}
if (propertyName.endsWith("[]")) {
propertyName = propertyName.substring(0, propertyName.length() - 2);
collectionProperty = true;
}
Field field = getField(type, propertyName);
if (collectionProperty && nestedPropertyName != null) {
LOG.debug("Cannot populate nested field on collection property {}", key);
throw new UnsupportedOperationException("Invalid field: " + key);
}
if (collectionProperty) {
// set collection value
List list = (List) getOrInitialize(instance, field, () -> new ArrayList());
values.forEach(v -> list.add(v));
} else {
if (nestedPropertyName != null) {
// set nested value
Class<?> nestedType = field.getType();
Object nestedObject = getOrInitialize(instance, field, () -> createInstance(nestedType));
populateField(nestedType, nestedObject, nestedPropertyName, values);
} else {
// set single value
if (values.size() > 1) {
LOG.debug("Multiple values in non-collection property {}", key);
throw new BadRequestException("Multiple values in non-collection property " + key);
}
set(type, instance, field, values.get(0));
}
}
}
static Field getField(Class<?> type, String fieldName) {
try {
return type.getDeclaredField(fieldName);
} catch (NoSuchFieldException | SecurityException e) {
LOG.debug("Not a field: {}#{}", type.getName(), fieldName, e);
throw new BadRequestException("Invalid field " + fieldName, e);
}
}
static <T> T safeAccess(Field field, Function<Field, T> accessFn) {
boolean oldAccessible = field.isAccessible();
try {
field.setAccessible(true);
return accessFn.apply(field);
} finally {
field.setAccessible(oldAccessible);
}
}
static <T> T getOrInitialize(Object o, Field field, Supplier<T> create) {
return safeAccess(field, (f) -> {
try {
T instance = (T) f.get(o);
if (instance == null) {
LOG.debug("Initialize {}#{}", o.getClass().getName(), f.getName());
instance = create.get();
f.set(o, instance);
}
return instance;
} catch (ClassCastException | IllegalAccessException | IllegalArgumentException e) {
LOG.error("Could not get/initialize field {}#{}", o.getClass().getName(), f.getName(), e);
throw new InternalServerErrorException(e);
}
});
}
static void set(Class<?> type, Object o, Field field, String value) {
Object actualValue;
if (value.isEmpty()) {
return;
}
if (field.getType().isAssignableFrom(Boolean.class)) {
actualValue = Boolean.parseBoolean(value);
} else if (field.getType().isAssignableFrom(Integer.class)) {
actualValue = Integer.parseInt(value, 10);
} else if (field.getType().isAssignableFrom(Long.class)) {
actualValue = Long.parseLong(value, 10);
} else if (field.getType().isAssignableFrom(Double.class)) {
actualValue = Double.parseDouble(value);
} else if (field.getType().isAssignableFrom(Date.class)) {
try {
actualValue = FormUtil.parseDate(value);
} catch (ParseException e) {
LOG.debug("Invalid date value: {}", value, e);
throw new BadRequestException("Invalid date: " + value, e);
}
} else {
actualValue = value;
}
LOG.debug("Coherced {} to {}", value, actualValue);
safeAccess(field, (f) -> {
try {
f.set(o, actualValue);
return null;
} catch (IllegalAccessException | IllegalArgumentException e) {
LOG.error("Failed to populate field {}#{}", type.getName(), f.getName(), e);
throw new InternalServerErrorException(
"Failed to populate " + type.getName() + "#" + f.getName(), e);
}
});
}
}