/*
* Copyright 2016 Stormpath, Inc.
*
* 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 com.stormpath.sdk.convert;
import com.stormpath.sdk.impl.resource.AbstractResource;
import com.stormpath.sdk.lang.Assert;
import com.stormpath.sdk.lang.Function;
import com.stormpath.sdk.lang.Instants;
import com.stormpath.sdk.lang.Strings;
import com.stormpath.sdk.resource.CollectionResource;
import com.stormpath.sdk.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Converts a {@link Resource} or {@link Map} to an output value that is structured according the specified
* {@link #setConfig(Conversion) config}.
*
* @since 1.3.0
*/
public class ResourceConverter<T> implements Function<T, Object> {
private static final Logger log = LoggerFactory.getLogger(ResourceConverter.class);
public static final Conversion DEFAULT_CONFIG = Conversions
//.withField("href", Conversions.disabled())
.withField("customData", Conversions.withStrategy(ConversionStrategyName.SCALARS))
.withField("groups", Conversions.withStrategy(ConversionStrategyName.DEFINED).setElements(Conversions.each(new Conversion())));
private Conversion config;
public ResourceConverter() {
this.config = DEFAULT_CONFIG;
}
/**
* Returns the {@code Conversion} that indicates how to convert the function argument into an output value.
*
* @return the {@code Conversion} that indicates how to convert the function argument into an output value.
*/
public Conversion getConfig() {
return config;
}
/**
* Sets the {@code Conversion} that indicates how to convert the function argument into an output value.
*
* @param config the {@code Conversion} that indicates how to convert the function argument into an output value.
*/
public void setConfig(Conversion config) {
Assert.notNull(config, "Conversion argument cannot be null.");
this.config = config;
}
/**
* a {@link Resource} or {@link Map} to an output value that is structured according the specified
* {@link #setConfig(Conversion) config}.
*
* @param t the resource or Map to convert
* @return an output value that is constructed according to the specified {@link #setConfig(Conversion) config}.
*/
@SuppressWarnings("unchecked")
@Override
public Object apply(T t) {
Assert.notNull(t, "Argument cannot be null");
return convert(t, this.config, "");
}
public Object convert(Object o, Conversion config, String path) {
Assert.notNull(o, "Object argument cannot be null");
ConversionStrategyName strategy = config.getStrategy();
Assert.notNull(strategy, "config strategy value cannot be null.");
if (o instanceof AbstractResource) {
AbstractResource ar = (AbstractResource) o;
if (!ar.isMaterialized()) {
ar.materialize();
}
}
boolean isCollection = o instanceof Iterable;
Map<String, Object> props = new LinkedHashMap<>();
if (strategy == ConversionStrategyName.SINGLE) {
String fieldName = config.getField();
if (Strings.hasText(fieldName) && hasProperty(o, fieldName)) {
return getProperty(o, fieldName);
} else if (o instanceof AbstractResource) {
return ((AbstractResource) o).getHref();
} else {
return null; //TODO: error or log message?
}
}
Map<String, Conversion> fieldsConfig = config.getFields();
Set<String> fieldNames = getPropertyNames(o);
for (String fieldName : fieldNames) {
String outputName = fieldName;
Object value = null;
boolean defined;
boolean enabled;
//if the current object is a collection, the elements in the collection are not represented by the
//fields config - instead they are represented by the 'elements' config. So if we're visiting an 'items'
//field, we need to see if it is enabled by checking the 'elements' config, not the 'fields' config:
if (isCollection && fieldName.equals("items")) {
ElementsConversion elementsConfig = config.getElements();
defined = elementsConfig != null;
enabled = !defined || elementsConfig.isEnabled();
} else {
//a field - check the fieldsConfig:
defined = fieldsConfig.containsKey(fieldName);
enabled = isEnabled(fieldsConfig, fieldName);
}
if (strategy == ConversionStrategyName.DEFINED) {
enabled = defined && enabled;
}
if (enabled) {
value = getProperty(o, fieldName);
}
boolean compound = isCompound(value);
if (strategy == ConversionStrategyName.SCALARS && compound && !defined) {
enabled = false;
}
if (enabled) {
Conversion fieldConfig = fieldsConfig.get(fieldName);
if (fieldConfig != null) {
String name = fieldConfig.getName();
if (Strings.hasText(name)) {
outputName = name;
}
}
if (compound) {
if (isCollection) {
String elementsPath = joinPath(path, "elements");
ElementsConversion elementsConfig = config.getElements();
String eachPath = joinPath(elementsPath, "each");
Conversion elementConfig = elementsConfig.getEach();
List<Object> elements = new ArrayList<>();
int i = 0;
for (Object element : ((Iterable) o)) {
Object elementValue = convert(element, elementConfig, eachPath + "[" + i + "]");
if (elementValue != null) {
elements.add(elementValue);
}
i++;
}
value = elements;
if (strategy == ConversionStrategyName.LIST) {
return value;
}
String elementsName = elementsConfig.getName();
if (Strings.hasText(elementsName)) {
outputName = elementsName;
}
} else {
String fieldsPath = joinPath(path, "fields");
fieldConfig = fieldsConfig.get(fieldName);
String newPath = joinPath(fieldsPath, fieldName);
value = convert(value, fieldConfig, newPath);
}
}
props.put(outputName, value);
}
}
if (props.isEmpty()) {
//nothing configured, TODO: print warning?
if (o instanceof AbstractResource) {
props.put(AbstractResource.HREF_PROP_NAME, ((AbstractResource) o).getHref());
}
}
return props;
}
@SuppressWarnings("unchecked")
protected Set<String> getPropertyNames(Object o) {
if (o instanceof AbstractResource) {
return ((AbstractResource) o).getPropertyNames();
} else if (o instanceof Map) {
return ((Map) o).keySet();
}
throw new IllegalArgumentException("Argument must be an AbstractResource or Map.");
}
protected boolean hasProperty(Object o, String name) {
if (o instanceof AbstractResource) {
return ((AbstractResource) o).hasProperty(name);
} else if (o instanceof Map) {
return ((Map) o).containsKey(name);
}
throw new IllegalArgumentException("Argument must be an AbstractResource or Map.");
}
protected Object getProperty(Object o, String name) {
Object value;
if (o instanceof Map) {
value = ((Map) o).get(name);
} else if (o instanceof AbstractResource) {
AbstractResource resource = (AbstractResource) o;
if (o instanceof CollectionResource && name.equals("items")) {
value = resource.getProperty(name);
} else {
try {
final Class<? extends AbstractResource> resourceClass = resource.getClass();
final String methodName = "get" + Strings.capitalize(name);
Method method = resourceClass.getMethod(methodName);
value = method.invoke(o);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
if (log.isDebugEnabled()) {
String msg = "Unable to access resource property '" + name + "': " + e.getMessage();
log.debug(msg, e);
}
value = resource.getProperty(name);
}
}
} else {
throw new IllegalArgumentException("Argument must be an AbstractResource or Map.");
}
if (value instanceof Date) {
Date date = (Date) value;
value = Instants.toUtcIso8601(date);
}
return value;
}
protected boolean isCompound(Object value) {
return value instanceof Collection || value instanceof Map || value instanceof Resource;
}
protected boolean isEnabled(Map<String, Conversion> fieldConfig, String field) {
if ("password".equals(field)) {
return false;
}
Conversion config = fieldConfig.get(field);
return config == null || config.isEnabled();
}
protected String joinPath(String parent, String child) {
StringBuilder sb = new StringBuilder(parent);
if (!"".equals(parent)) {
sb.append('.');
}
sb.append(child);
return sb.toString();
}
}