diff --git a/engine/src/main/java/org/archive/crawler/restlet/BeanBrowseResource.java b/engine/src/main/java/org/archive/crawler/restlet/BeanBrowseResource.java index 95b15d84..2a0e9701 100644 --- a/engine/src/main/java/org/archive/crawler/restlet/BeanBrowseResource.java +++ b/engine/src/main/java/org/archive/crawler/restlet/BeanBrowseResource.java @@ -64,6 +64,7 @@ public class BeanBrowseResource extends JobRelatedResource { super.init(ctx, req, res); getVariants().add(new Variant(MediaType.TEXT_HTML)); getVariants().add(new Variant(MediaType.APPLICATION_XML)); + getVariants().add(new Variant(MediaType.APPLICATION_JSON)); appCtx = cj.getJobContext(); beanPath = (String)req.getAttributes().get("beanPath"); if (beanPath!=null) { @@ -129,6 +130,12 @@ public class BeanBrowseResource extends JobRelatedResource { XmlMarshaller.marshalDocument(writer, "beans", makeDataModel()); } }; + } else if (variant.getMediaType() == MediaType.APPLICATION_JSON) { + return new WriterRepresentation(MediaType.APPLICATION_JSON) { + public void write(Writer writer) throws IOException { + JsonMarshaller.marshalDocument(writer, null, makeDataModel()); + } + }; } else { ViewModel viewModel = new ViewModel(); viewModel.put("model", makeDataModel()); diff --git a/engine/src/main/java/org/archive/crawler/restlet/JsonMarshaller.java b/engine/src/main/java/org/archive/crawler/restlet/JsonMarshaller.java new file mode 100644 index 00000000..a497b84a --- /dev/null +++ b/engine/src/main/java/org/archive/crawler/restlet/JsonMarshaller.java @@ -0,0 +1,210 @@ +package org.archive.crawler.restlet; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.restlet.data.Reference; + +import jakarta.xml.bind.annotation.XmlTransient; +import jakarta.xml.bind.annotation.XmlType; + +import static org.archive.crawler.restlet.XmlMarshaller.marshalAsElement; +import static org.archive.crawler.restlet.XmlMarshaller.orderProperties; + +/** + * JsonMarshaller can be used to write data structures as simple json. See + * {@link #marshalDocument(Writer, String, Object)} for more information. + * + * @author ekoerner + * @see {@link XmlMarshaller} + */ +public class JsonMarshaller { + + private static final Logger logger = Logger.getLogger(JsonMarshaller.class.getName()); + + /** + * Writes {@code content} as json to {@code writer}. Recursively descends + * into Maps, using keys as object keys. Iterates over items in arrays and + * Iterables, generating lists. Marshals simple object values + * with {@link #toString()}. The enclosing Object with {@code rootKey} + * will not be generated if {@code rootKey} is empty. + * The result looks something like this: + * + *
+ * { "rootKey": { // root object is a Map
+ * "key1": simpleObjectValue1,
+ * "key2": { // /rootKey/key2 is another Map
+ * "subkey1": subvalue1,
+ * "subkey2": [ // an array or Iterable
+ * item1Value,
+ * item2Value
+ * ],
+ * "subkey3": subvalue3
+ * }
+ * }
+ * }
+ *
+ *
+ * @param writer output writer
+ * @param rootKey (optional) wrapper root key name
+ * @param content data structure to marshal
+ * @throws IOException
+ * @see {@link XmlMarshaller#marshalDocument(Writer, String, Object)}
+ */
+ public static void marshalDocument(Writer writer, String rootKey, Object content) throws IOException {
+ // rethrow IOException
+ // rethrow JSONException (is already a RuntimeException)
+
+ Object wrapped = marshal(content);
+
+ if (!StringUtils.isEmpty(rootKey)) {
+ wrapped = new JSONObject().put(rootKey, wrapped);
+ }
+
+ serialize(writer, wrapped, 2);
+ }
+
+ /**
+ * Serialize json {@code value} to {@code writer}.
+ *
+ * @param writer output writer
+ * @param value value to be serialized
+ * @param indentFactor number of spaces for json indentation
+ * @throws IOException
+ * @see {@link JSONObject#writeValue(Writer, Object, int, int)}
+ */
+ protected static void serialize(Writer writer, Object value, int indentFactor) throws IOException {
+ // JSONObject.writeValue(writer, wrapped, 2, 0);
+ if (value instanceof JSONObject) {
+ ((JSONObject) value).write(writer, indentFactor, 0);
+ } else if (value instanceof JSONArray) {
+ ((JSONArray) value).write(writer, indentFactor, 0);
+ } else if (value == null) {
+ writer.write("null");
+ } else {
+ // we should usually not arrive here, I think
+ // we could alternatively just go with `value.toString()` (but this is not
+ // necessarily valid JSON)
+ throw new RuntimeException("Unexpected JSON marshalling result?");
+ }
+ }
+
+ /**
+ * Convert/marshall {@code value} to JSON objects if possible.
+ *
+ * @param value object to convert
+ * @return JSON object/array or {@code value} if not necessary because basic
+ * type
+ * @see {@link XmlMarshaller#marshal(org.restlet.ext.xml.XmlWriter, String, Object)}
+ * @see {@link JSONObject#wrap(Object)}
+ */
+ protected static Object marshal(Object value) {
+ // see JSONObject.wrap()
+ // xmlWriter.dataElement(key, stripInvalidXMLChars(value.toString()));
+ if (value == null || value instanceof Byte || value instanceof Character
+ || value instanceof Short || value instanceof Integer
+ || value instanceof Long || value instanceof Boolean
+ || value instanceof Float || value instanceof Double
+ || value instanceof String || value instanceof BigInteger
+ || value instanceof BigDecimal || value instanceof Enum) {
+ return value;
+ }
+
+ // handle known annoying circular references
+ if (value instanceof Reference) {
+ return value.toString();
+ }
+
+ if (value instanceof Map, ?>) {
+ Map, ?> map = (Map, ?>) value;
+ JSONObject obj = new JSONObject();
+ for (Map.Entry, ?> entry : map.entrySet()) {
+ obj.put(entry.getKey().toString(), marshal(entry.getValue()));
+ }
+ return obj;
+ }
+
+ if (value instanceof Iterable>) {
+ Iterable> iterable = (Iterable>) value;
+ JSONArray array = new JSONArray();
+ for (Object item : iterable) {
+ array.put(marshal(item));
+ }
+ return array;
+ }
+
+ if (marshalAsElement(value)) {
+ return marshalBean(value);
+ }
+
+ // so, here is the critical part where we want to avoid circular references as
+ // this is the catch-all!
+ try {
+ // let's just attempt to JSONify it
+ return new JSONObject(value);
+ } catch (StackOverflowError e) {
+ logger.fine("Infinite recursion for class=" + value.getClass());
+ }
+
+ return value.toString();
+ }
+
+ /**
+ * generate nested JSON structure for a bean {@code obj}.
+ * each readable JavaBeans property is mapped to an nested element named after
+ * its name. Those properties annotated with {@link XmlTransient} are ignored.
+ *
+ * @param obj bean
+ * @see {@link XmlMarshaller#marshalBean(org.restlet.ext.xml.XmlWriter, String, Object)}
+ */
+ protected static JSONObject marshalBean(Object obj) {
+ // we will always return this object even if conversion fails partways?
+ JSONObject object = new JSONObject();
+
+ try {
+ BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass(), Object.class);
+ PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
+
+ XmlType xmlType = obj.getClass().getAnnotation(XmlType.class);
+ if (xmlType != null) {
+ String[] propOrder = xmlType.propOrder();
+ if (propOrder != null) {
+ // TODO: should cache this sorted version?
+ orderProperties(props, propOrder);
+ }
+ }
+
+ for (PropertyDescriptor prop : props) {
+ Method m = prop.getReadMethod();
+ if (m == null || m.getAnnotation(XmlTransient.class) != null)
+ continue;
+ try {
+ Object propValue = m.invoke(obj);
+ if (propValue != null && !"".equals(propValue)) {
+ object.put(prop.getName(), marshal(propValue));
+ }
+ } catch (Exception ex) {
+ // generate empty element, for now. generate comment?
+ // TODO: generate null or ""? (Or skip completely?)
+ object.put(prop.getName(), "");
+ }
+ }
+ } catch (IntrospectionException ex) {
+ // ignored, for now.
+ }
+
+ return object;
+ }
+}