Commit e2191972 authored by Martti Käärik's avatar Martti Käärik
Browse files

Rendering of special values (common + plug into HTTP adapter) #224

parent d33660df
Loading
Loading
Loading
Loading
+95 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.adapters;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

/**
 * Reflective access to POJO members shared by {@link PojoValidator} and
 * {@link PojoRenderer}: the members of an object are its non-static fields
 * (including superclasses), read through the JavaBeans getter if the class
 * provides one and directly otherwise.
 */
final class PojoReflection {

	private PojoReflection() {
	}

	/**
	 * A readable member of a class.
	 */
	static final class Member {
		final Field field;
		final Method getter;

		Member(Field field, Method getter) {
			this.field = field;
			this.getter = getter;
		}

		String getName() {
			return field.getName();
		}

		Object read(Object target) {
			try {
				if (getter != null)
					return getter.invoke(target);
				field.setAccessible(true);
				return field.get(target);
			} catch (IllegalAccessException | InvocationTargetException e) {
				throw new AssertionError("Cannot read " + field.getDeclaringClass().getName() + "." + field.getName(),
						e);
			}
		}
	}

	/**
	 * The members of a class: all non-static, non-synthetic fields declared by the
	 * class and its superclasses (excluding {@link Object}), in declaration order.
	 */
	static List<Member> members(Class<?> type) {
		List<Member> members = new ArrayList<>();
		for (Class<?> c = type; c != null && c != Object.class; c = c.getSuperclass())
			for (Field f : c.getDeclaredFields()) {
				if (Modifier.isStatic(f.getModifiers()) || f.isSynthetic())
					continue;
				members.add(new Member(f, findGetter(c, f)));
			}
		return members;
	}

	private static Method findGetter(Class<?> c, Field f) {
		String name = f.getName();
		String suffix = Character.toUpperCase(name.charAt(0)) + name.substring(1);
		String[] candidates = f.getType() == boolean.class || f.getType() == Boolean.class
				? new String[] { "is" + suffix, "get" + suffix }
				: new String[] { "get" + suffix };
		for (String candidate : candidates) {
			try {
				Method m = c.getMethod(candidate);
				if (!Modifier.isStatic(m.getModifiers()) && m.getReturnType() != void.class)
					return m;
			} catch (NoSuchMethodException e) {
				// try next
			}
		}
		return null;
	}

	/**
	 * Whether an object is a leaf that is compared/rendered as a whole rather than
	 * member by member: enums and JDK types (String, boxed primitives, temporal
	 * types, BigDecimal, ...).
	 */
	static boolean isLeaf(Object o) {
		if (o instanceof Enum)
			return true;
		String p = o.getClass().getPackageName();
		return p.startsWith("java.") || p.startsWith("javax.");
	}

}
+136 −0
Original line number Diff line number Diff line
package org.etsi.mts.tdl.execution.java.adapters;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.etsi.mts.tdl.execution.java.rt.core.TemplateImpl;
import org.etsi.mts.tdl.execution.java.tri.Data;
import org.etsi.mts.tdl.execution.java.tri.SpecialValue;
import org.etsi.mts.tdl.execution.java.tri.Template;

/**
 * Renders mapped data for logging, from its value and template. The notation
 * follows TDL: <code>?</code> for <i>AnyValue</i>, <code>*</code> for
 * <i>AnyValueOrOmit</i>, <code>omit</code> for <i>OmitValue</i>. The rest is
 * JSON-like. Members that are unassigned at TDL level are shown with the
 * treatment in effect for them, or left out if none is defined.
 */
public final class PojoRenderer {

	private PojoRenderer() {
	}

	/**
	 * Render expected data.
	 */
	public static String render(Data<?, ?> data) {
		if (data == null)
			return "null";
		return render(data.getValue(), data.getTemplate());
	}

	public static String render(Object value, Template template) {
		StringBuilder out = new StringBuilder();
		render(value, template != null ? template : TemplateImpl.NONE, out);
		return out.toString();
	}

	public static String renderMember(Object value, Template container, String name) {
		Template t = (container != null ? container : TemplateImpl.NONE).getMember(name);
		if (t.getSpecialValue() == null && value == null)
			return symbol(container != null ? container.getUnassignedMemberTreatment() : null);
		return render(value, t);
	}

	private static void render(Object value, Template template, StringBuilder out) {
		if (template.getSpecialValue() != null) {
			out.append(symbol(template.getSpecialValue()));
			return;
		}
		if (value == null) {
			out.append("null");
			return;
		}
		if (value instanceof CharSequence || value instanceof Character) {
			out.append('"').append(value).append('"');
			return;
		}
		if (PojoReflection.isLeaf(value) && !(value instanceof List) && !(value instanceof Map)) {
			out.append(value);
			return;
		}
		if (value instanceof List) {
			out.append('[');
			Iterator<?> it = ((List<?>) value).iterator();
			for (int i = 0; it.hasNext(); i++) {
				if (i > 0)
					out.append(", ");
				renderPart(it.next(), template.getItem(i), template, out);
			}
			out.append(']');
			return;
		}
		out.append('{');
		boolean first = true;
		if (value instanceof Map) {
			for (Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet())
				first = renderNamed(String.valueOf(e.getKey()), e.getValue(), template, first, out);
		} else {
			for (PojoReflection.Member m : PojoReflection.members(value.getClass()))
				first = renderNamed(m.getName(), m.read(value), template, first, out);
		}
		out.append('}');
	}

	private static boolean renderNamed(String name, Object value, Template container, boolean first,
			StringBuilder out) {
		Template t = container.getMember(name);
		if (t.getSpecialValue() == null && value == null) {
			String treatment = symbol(container.getUnassignedMemberTreatment());
			if (treatment == null)
				// Unassigned and no treatment defined: left to the validator default
				return first;
			if (!first)
				out.append(", ");
			out.append(name).append(": ").append(treatment);
			return false;
		}
		if (!first)
			out.append(", ");
		out.append(name).append(": ");
		render(value, t, out);
		return false;
	}

	private static void renderPart(Object value, Template t, Template container, StringBuilder out) {
		if (t.getSpecialValue() == null && value == null) {
			String treatment = symbol(container.getUnassignedMemberTreatment());
			out.append(treatment != null ? treatment : "null");
			return;
		}
		render(value, t, out);
	}

	/**
	 * The TDL notation of a special value.
	 *
	 * @return <code>?</code>, <code>*</code> or <code>omit</code>, or <b>null</b>
	 *         for <b>null</b>.
	 */
	public static String symbol(SpecialValue v) {
		if (v == null)
			return null;
		switch (v) {
		case AnyValue:
			return "?";
		case AnyValueOrOmit:
			return "*";
		case OmitValue:
			return "omit";
		default:
			return v.name();
		}
	}

}
+52 −28
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Pattern;

import org.etsi.mts.tdl.execution.java.adapters.PojoRenderer;
import org.etsi.mts.tdl.execution.java.rt.core.PojoData;
import org.etsi.mts.tdl.execution.java.tri.Argument;
import org.etsi.mts.tdl.execution.java.tri.ComponentInstanceRole;
@@ -41,6 +42,7 @@ import org.etsi.mts.tdl.execution.java.tri.NamedElement;
import org.etsi.mts.tdl.execution.java.tri.Procedure;
import org.etsi.mts.tdl.execution.java.tri.Reporter;
import org.etsi.mts.tdl.execution.java.tri.SystemAdapter;
import org.etsi.mts.tdl.execution.java.tri.Template;
import org.etsi.mts.tdl.execution.java.tri.Type;
import org.etsi.mts.tdl.execution.java.tri.Validator;
import org.etsi.mts.tdl.execution.java.tri.Value;
@@ -201,6 +203,8 @@ public class HttpSystemAdapter implements SystemAdapter {
		if (t != null)
			handleError(t);
		else {
			reporter.log((target != null ? target.componentName : defaultSut),
					"SystemAdapter | " + "Incoming: " + response.statusCode() + " | " + toDefault(response.body(), "<empty body>"));
			synchronized (unhandledInputs) {
				unhandledInputs.add(new HttpInput(response, null, target, connection));
				unhandledInputs.notifyAll();
@@ -306,10 +310,10 @@ public class HttpSystemAdapter implements SystemAdapter {
				requestBuilder.uri(uri);

				// TODO logging
				reporter.comment("SystemAdapter | " + source.getName(), "Outgoing (headers): " + httpData.method.toString() + " | " + uri);
				reporter.log(source.getName(), "SystemAdapter | " + "Outgoing (headers): " + httpData.method.toString() + " | " + uri);
				
				byte[] body = encodeBody(httpData.body);
				reporter.comment("SystemAdapter | " + source.getName(), "Outgoing (body): " + toDefault(mapper.writeValueAsString(httpData.body), "<empty body>"));
				reporter.log(source.getName(), "SystemAdapter | " + "Outgoing (body): " + toDefault(mapper.writeValueAsString(httpData.body), "<empty body>"));
				requestBuilder.method(httpData.method.toString(), HttpRequest.BodyPublishers.ofByteArray(body));

				if (httpData.headers != null)
@@ -363,8 +367,8 @@ public class HttpSystemAdapter implements SystemAdapter {
				int length = body.length;

				// TODO logging
				reporter.comment("SystemAdapter | " + source.getName(), "Outgoing (headers): " + httpData.status + " | length=" + length);
				reporter.comment("SystemAdapter | " + source.getName(), "Outgoing (body): " + toDefault(mapper.writeValueAsString(httpData.body), "<empty body>"));
				reporter.log(source.getName(), "SystemAdapter | " + "Outgoing (headers): " + httpData.status + " | length=" + length);
				reporter.log(source.getName(), "SystemAdapter | " + "Outgoing (body): " + toDefault(mapper.writeValueAsString(httpData.body), "<empty body>"));
				
				unhandledExchange.sendResponseHeaders(httpData.status, length);
				
@@ -408,8 +412,6 @@ public class HttpSystemAdapter implements SystemAdapter {

				String body = response.body();

				// TODO logging
				//reporter.comment(target.getName(), "Incoming: " + response.statusCode() + " | " + body);

				try {
					
@@ -427,16 +429,12 @@ public class HttpSystemAdapter implements SystemAdapter {

					Data received = new PojoData<>(receivedHttpData);
					if (expected != null) {
						String expectedBody = null;
						if (expectedHttpData.body != null) {
						if (expectedHttpData.body != null)
							receivedHttpData.body = decodeBody(body, expectedHttpData.body.getClass());
							// For logging only
							expectedBody = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(encodeBody(expectedHttpData.body))).toString();
						}
						reporter.comment("SystemAdapter | " + target.getName(), "Matching: " + response.statusCode() + " | " + toDefault(body, "<empty body>"));
						reporter.comment("SystemAdapter | " + target.getName(), "\t vs " + toAny(expectedHttpData.status) + " | " + toAny(expectedBody));
						reporter.log(target.getName(), "SystemAdapter | " + "Matching:\t   " + response.statusCode() + " | " + toDefault(body, "<empty body>"));
						reporter.log(target.getName(), "SystemAdapter | " + "\t expected: " + describeExpected(expected, "status"));
						if (this.validator.matches(expected, received)) {
							reporter.comment("SystemAdapter | " + target.getName(), "\t MATCHED");
							reporter.log(target.getName(), "SystemAdapter | " + "\t MATCHED");
							unhandledInputs.remove(input);
							return received;
						}
@@ -465,9 +463,8 @@ public class HttpSystemAdapter implements SystemAdapter {
						// TODO other encodings
						Charset cs = StandardCharsets.UTF_8;
						input.body = new String(is.readAllBytes(), cs);
						reporter.log(target.getName(), "SystemAdapter | " + "Incoming: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " | " + toDefault(input.body, "<empty body>"));

						// TODO logging
						// reporter.comment(target.getName(), "Incoming: " + exchange.getRequestMethod() + " | " + input.body);
	
					} catch (IOException e) {
						handleError(e);
@@ -488,16 +485,12 @@ public class HttpSystemAdapter implements SystemAdapter {
					Data received = new PojoData<>(receivedHttpData);
					boolean isReceived = false;
					if (expected != null) {
						String expectedBody = null;
						if (expectedHttpData.body != null) {
						if (expectedHttpData.body != null)
							receivedHttpData.body = decodeBody(input.body, expectedHttpData.body.getClass());
							// For logging only
							expectedBody  = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(encodeBody(expectedHttpData.body))).toString();
						}
						reporter.comment("SystemAdapter | " + target.getName(), "Matching: " + exchange.getRequestMethod() + " | " + toDefault(input.body, "<empty body>"));
						reporter.comment("SystemAdapter | " + target.getName(), "\t vs " + toAny(expectedHttpData.method) + " | " + toAny(expectedBody));
						reporter.log(target.getName(), "SystemAdapter | " + "Matching:\t   " + exchange.getRequestMethod() + " | " + toDefault(input.body, "<empty body>"));
						reporter.log(target.getName(), "SystemAdapter | " + "\t expected: " + describeExpected(expected, "method"));
						if (this.validator.matches(expected, received)) {
							reporter.comment("SystemAdapter | " + target.getName(), "\t MATCHED");
							reporter.log(target.getName(), "SystemAdapter | " + "\t MATCHED");
							isReceived = true;
						}

@@ -527,19 +520,50 @@ public class HttpSystemAdapter implements SystemAdapter {
		return null;
	}
	
	private String toAny(Object value) {
		return toDefault(value, "*");
	}
	
	private String toDefault(Object value, String def) {
		return value != null && !value.equals("null") ? value.toString() : def;
	}

	/**
	 * Renders expected request/response data as "head | body" from its value and
	 * template, so that special values (<code>?</code>, <code>*</code>,
	 * <code>omit</code>) and unassigned parts show as such rather than as the
	 * placeholders the mapped object holds for them.
	 *
	 * @param headMember The member rendered as head: "status" for responses,
	 *                   "method" for requests.
	 */
	private String describeExpected(Data expected, String headMember) {
		Object value = expected.getValue();
		Template template = expected.getTemplate();
		if (template.getSpecialValue() != null)
			// The whole message is a special value
			return PojoRenderer.render(expected);
		Object head = null;
		Object body = null;
		if (value instanceof HttpRequestData) {
			head = ((HttpRequestData) value).method;
			body = ((HttpRequestData) value).body;
		} else if (value instanceof HttpResponseData) {
			head = ((HttpResponseData) value).status;
			body = ((HttpResponseData) value).body;
		} else {
			return PojoRenderer.render(expected);
		}
		// An unassigned head/body with no treatment defined is left to the
		// validator default; shown as "*" to keep the log readable
		return toDefault(PojoRenderer.renderMember(head, template, headMember), "*") + " | "
				+ toDefault(PojoRenderer.renderMember(body, template, "body"), "*");
	}

	@Override
	public void logUnexpected(Data received, Connection connection, NamedElement target) {
		reporter.log(target.getName(), "SystemAdapter | " + "UNEXPECTED: " + describe(received));
	}

	/**
	 * Renders received request/response data as "head | body".
	 */
	private String describe(Data data) {
		Object value = data.getValue();
		Object head;