/* * The MIT License * * Copyright 2014 Tim Boudreau. * * 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 com.mastfrog.acteur; import com.google.common.net.MediaType; import com.google.inject.Provider; import com.mastfrog.giulius.Dependencies; import com.mastfrog.parameters.KeysValues; import com.mastfrog.parameters.gen.Origin; import com.mastfrog.parameters.validation.ParamChecker; import com.mastfrog.util.Codec; import com.mastfrog.util.Streams; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.nio.charset.Charset; import java.util.Date; import java.util.Map; import javax.inject.Inject; import org.joda.time.DateTime; import org.joda.time.Duration; import org.netbeans.validation.api.InvalidInputException; import org.netbeans.validation.api.Problems; /** * Converts byte buffers and maps to objects * * @author Tim Boudreau */ public class ContentConverter { protected final Codec codec; private final Provider<Charset> charset; private final ParamChecker checker; private final Dependencies deps; @Inject public ContentConverter(Codec codec, Provider<Charset> charset, ParamChecker checker, Dependencies deps) { this.codec = codec; this.charset = charset; this.checker = checker; this.deps = deps; } public String toString(ByteBuf content, Charset encoding) throws IOException { String result; try (ByteBufInputStream in = new ByteBufInputStream(content)) { result = Streams.readString(in, encoding.toString()); } finally { content.resetReaderIndex(); } if (result.length() > 0 && result.charAt(0) == '"') { result = result.substring(1); } if (result.length() > 1 && result.charAt(result.length() - 1) == '"') { result = result.substring(0, result.length() - 2); } return result; } private Charset findCharset(MediaType mt) { if (mt == null) { return charset.get(); } if (mt.charset().isPresent()) { return mt.charset().get(); } return charset.get(); } @SuppressWarnings("unchecked") public <T> T toObject(ByteBuf content, MediaType mimeType, Class<T> type) throws IOException { if (mimeType == null) { mimeType = MediaType.ANY_TYPE; } // Special handling for strings if (type == String.class || type == CharSequence.class) { return type.cast(toString(content, findCharset(mimeType))); } if (type.isInterface()) { Map<String, Object> m; try (InputStream in = new ByteBufInputStream(content)) { m = codec.readValue(in, Map.class); } return toObject(m, type); } return readObject(content, mimeType, type); } protected <T> T readObject(ByteBuf buf, MediaType mimeType, Class<T> type) throws IOException, InvalidInputException { if (type == String.class || type == CharSequence.class) { return type.cast(toString(buf, findCharset(mimeType))); } Origin origin = type.getAnnotation(Origin.class); if (origin != null) { Map map; try (InputStream in = new ByteBufInputStream(buf)) { map = codec.readValue(in, Map.class); validate(origin, map).throwIfFatalPresent(); } catch (IOException ioe) { ioe.printStackTrace(); throw ioe; } finally { buf.resetReaderIndex(); } } buf.resetReaderIndex(); try (InputStream in = new ByteBufInputStream(buf)) { T result = codec.readValue(in, type); return result; } catch (IOException ioe) { ioe.printStackTrace(); throw ioe; } finally { buf.resetReaderIndex(); } } @SuppressWarnings("unchecked") private Problems validate(Origin origin, Map map) { Problems problems = new Problems(); checker.check(origin.value(), new KeysValues.MapAdapter(map), problems); return problems; } public <T> T toObject(Map<String, ?> m, Class<T> type) throws InvalidInputException { if (type.isInterface()) { return createProxyFor(m, type); } else { return createObjectFor(m, type); } } protected <T> T createObjectFor(Map<String, ?> m, Class<T> type) { Origin origin = type.getAnnotation(Origin.class); if (origin != null) { validate(origin, m).throwIfFatalPresent(); } return deps.getInstance(type); } @SuppressWarnings("unchecked") protected <T> T createProxyFor(Map<String, ?> m, Class<T> type) { if (!type.isInterface()) { throw new IllegalArgumentException("Not an interface: " + type); } return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{type}, new IH(type, m)); } static class IH implements InvocationHandler { private final Class<?> iface; private final Map<String, ?> map; IH(Class<?> iface, Map<String, ?> map) { this.iface = iface; this.map = map; } @Override public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { return false; } if ("hashCode".equals(method.getName())) { return map.hashCode(); } if ("toString".equals(method.getName())) { return "Proxy " + iface.getSimpleName() + " over parameters " + map; } String nm = method.getName(); String result = map.get(nm) instanceof String ? (String) map.get(nm) : map.containsKey(nm) ? map.get(nm) + "" : null; Class<?> ret = method.getReturnType(); if (result == null) { return null; } else if (ret == Long.TYPE || ret == Long.class) { if (ret == Long.class && result == null) { return null; } return Long.parseLong(result); } else if (ret == String.class || ret == CharSequence.class) { return result; } else if (ret == Integer.TYPE || ret == Integer.class) { if (ret == Integer.class && result == null) { return null; } return Integer.parseInt(result); } else if (ret == Double.TYPE || ret == Double.class || ret == Number.class) { if (ret == Double.class && result == null) { return null; } return Double.parseDouble(result); } else if (ret == Float.TYPE || ret == Float.class) { if (ret == Float.class && result == null) { return null; } return Float.parseFloat(result); } else if (ret == char[].class) { return result.toCharArray(); } else if (Byte.TYPE == ret || Byte.class == ret) { if (ret == Byte.class && result == null) { return null; } return Byte.parseByte(result); } else if (Short.class == ret || Short.TYPE == ret) { if (ret == Short.class && result == null) { return null; } return Short.parseShort(result); } else if (ret == Boolean.TYPE || ret == Boolean.class) { switch (result) { case "0": return false; case "1": return true; default: return result == null ? false : Boolean.parseBoolean(result); } } else if (method.getReturnType() == String.class) { return result.split(","); } else if (method.getReturnType() == Date.class) { long when = parseDate(result); if (when != Long.MIN_VALUE) { return parseDate(result); } return null; } else if (method.getReturnType() == DateTime.class) { long when = parseDate(result); if (when == Long.MIN_VALUE) { return null; } return new DateTime(parseDate(result)); } else if (method.getReturnType() == Duration.class) { long amt = -1; try { amt = Long.parseLong(result); } catch (NumberFormatException nfe) { return Duration.ZERO; } return new Duration(amt); } throw new IllegalArgumentException("Unsupported type " + method.getReturnType()); } } private static long parseDate(String result) { long when; try { when = Long.parseLong(result); } catch (NumberFormatException nfe) { when = Date.parse(result); } return when; } }