cancel
Showing results for 
Search instead for 
Did you mean: 

JSON array evaluating to StringModel in FreeMarker

stuartleyland
Champ on-the-rise
Champ on-the-rise
Hello,

I have a web script in Share that calls a repo web script which returns some JSON. I want to use this JSON in a FreeMarker template to build up a page but I'm having some problems. The repo side seems to be working absolutely fine. Here is an example of the JSON it returns:

[{"Name":"Matrix - Connectivity problems","Problem":"Terminal won't connect\nConnection error"}]‍‍‍


This is my Share side Java controller which calls the repo web script and puts the results into the model.

package com.x.y.z;import java.util.HashMap;import java.util.Map;import org.alfresco.error.AlfrescoRuntimeException;import org.alfresco.util.ParameterCheck;import org.apache.commons.lang3.StringUtils;import org.json.JSONArray;import org.json.JSONException;import org.springframework.extensions.surf.RequestContext;import org.springframework.extensions.surf.ServletUtil;import org.springframework.extensions.surf.exception.ConnectorServiceException;import org.springframework.extensions.surf.support.ThreadLocalRequestContext;import org.springframework.extensions.webscripts.Cache;import org.springframework.extensions.webscripts.DeclarativeWebScript;import org.springframework.extensions.webscripts.Status;import org.springframework.extensions.webscripts.WebScriptException;import org.springframework.extensions.webscripts.WebScriptRequest;import org.springframework.extensions.webscripts.WebScriptSession;import org.springframework.extensions.webscripts.connector.Connector;import org.springframework.extensions.webscripts.connector.Response;public class ShareWebScript extends DeclarativeWebScript {      @Override   protected Map<String,Object> executeImpl(WebScriptRequest req, Status status) {      return executeImpl(req, status, null);   }      @Override   protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache) {      String query = getParameterAsString(req, "query");         Map<String, Object> model = new HashMap<>();            try {         final RequestContext rc = ThreadLocalRequestContext.getRequestContext();         final String userId = rc.getUserId();            final Connector conn = rc.getServiceRegistry().getConnectorService().getConnector("alfresco", userId, ServletUtil.getSession());         final String url = "/repo/search?query=" + query;         final Response response = conn.call(url);            if (response.getStatus().getCode() == Status.STATUS_OK) {            JSONArray resultsResponse = new JSONArray(response.getResponse());            model.put("results", resultsResponse);         }      } catch (final ConnectorServiceException e)      {         throw new AlfrescoRuntimeException("Failed to connect repository", e);         } catch (final JSONException e)      {         throw new AlfrescoRuntimeException("Failed to parse JSON string", e);      }            return model;   }      private String getParameterAsString(final WebScriptRequest req, final String paramName) {           ParameterCheck.mandatory("req", req);           ParameterCheck.mandatoryString("paramName", paramName);           String paramVal = StringUtils.trimToNull(req.getParameter(paramName));           if (StringUtils.isBlank(paramVal)) {                  throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Missing " + paramName + " parameter");           }           return paramVal;       }}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍


This is the FTL:

<#escape x as jsonUtils.encodeJSONString(x)>   {      "Results" : [         <#list results as result>            {               "Title" : "${result.Name}",               "Problem : "${result.Problem}"            },         </#list>      ]   }</#escape>‍‍‍‍‍‍‍‍‍‍‍‍‍‍


And the error I'm getting is:

freemarker.template.TemplateException: Expected collection or sequence. results evaluated instead to freemarker.ext.beans.StringModel on line 4‍‍‍


I have remotely debugged the application and can see that the JSONArray I create from the response in the controller is correctly formed. I have attached a screenshot showing this.

I'm sure I'm doing something simple wrong but I can't figure it out.
6 REPLIES 6

afaust
Legendary Innovator
Legendary Innovator
Hello,

the problem with this is that JSONArray is by default not a supported data type to put into the model for the template processor. You should use proper array, List or Collection values when you populate the model in Java. Any unsupported data type will result in the template processor to convert the object into StringModel via its toString() operation.

Regards
Axel

Brilliant! Thanks so much that fixed my problem. My working code is listed below for others who have the same problem in the future.

package com.x.y.z;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import org.alfresco.error.AlfrescoRuntimeException;import org.alfresco.util.ParameterCheck;import org.apache.commons.lang3.StringUtils;import org.json.JSONArray;import org.json.JSONException;import org.json.JSONObject;import org.springframework.extensions.surf.RequestContext;import org.springframework.extensions.surf.ServletUtil;import org.springframework.extensions.surf.exception.ConnectorServiceException;import org.springframework.extensions.surf.support.ThreadLocalRequestContext;import org.springframework.extensions.webscripts.Cache;import org.springframework.extensions.webscripts.DeclarativeWebScript;import org.springframework.extensions.webscripts.Status;import org.springframework.extensions.webscripts.WebScriptException;import org.springframework.extensions.webscripts.WebScriptRequest;import org.springframework.extensions.webscripts.WebScriptSession;import org.springframework.extensions.webscripts.connector.Connector;import org.springframework.extensions.webscripts.connector.Response;public class ShareWebScript extends DeclarativeWebScript {      @Override   protected Map<String,Object> executeImpl(WebScriptRequest req, Status status) {      return executeImpl(req, status, null);   }      @Override   protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache) {      Map<String, Object> model = new HashMap<>();      List<Map<String, String>> results = new ArrayList<>();            try {         final RequestContext rc = ThreadLocalRequestContext.getRequestContext();         final String userId = rc.getUserId();            final Connector conn = rc.getServiceRegistry().getConnectorService().getConnector("alfresco", userId, ServletUtil.getSession());         final String url = "/mySearch?query=" + query;         final Response response = conn.call(url);            if (response.getStatus().getCode() == Status.STATUS_OK) {            JSONArray resultsResponse = new JSONArray(response.getResponse());            for (int i = 0; i < resultsResponse.length(); i++) {               JSONObject result = resultsResponse.getJSONObject(i);               Map<String, String> resultObject = new HashMap<>();                              String title = result.getString("Name");               resultObject.put("ShareTitle", title);                              String problem = result.getString("Problem");               resultObject.put("ShareProblem", problem);                              results.add(resultObject);            }                        model.put("results", results);         }      } catch (final ConnectorServiceException e)      {         throw new AlfrescoRuntimeException("Failed to connect repository", e);         } catch (final JSONException e)      {         throw new AlfrescoRuntimeException("Failed to parse JSON string", e);      }            return model;   }    }}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍


And this is the FTL.

<#escape x as jsonUtils.encodeJSONString(x)>   {      "Results" : [         <#list results as result>            {               "Title" : "${result.ShareTitle}",               "Problem : "${result.ShareProblem}"            },         </#list>      ]   }</#escape>‍‍‍‍‍‍‍‍‍‍‍‍‍‍


Thanks again, much appreciated.

sorin_postelnic
Confirmed Champ
Confirmed Champ
I have a very similar problem, with the same error.
But in my case I am trying to use a javascript-based webscript.

To take the same example as Stuart Layland, but converted to javascript (The FTL is the same.):
<javascript>
var connector = remote.connect("alfresco-webscripts");
var webscriptUrl = "/mySearch?query=" + args.query;
var data = connector.get(webscriptUrl);
logger.log(data);
model.results = eval(data);
</javascript>

But I get the error: freemarker.template.TemplateException: Expected collection or sequence. results evaluated instead to freemarker.ext.beans.StringModel on line 4

Is it not possible to directly pass the eval'd object to the template model?
Do I really need to create a Java-based webscript for this?

Apart from the fact that you should avoid eval() altogether and instead use something like JSON.parse() to avoid code injection issues, you should be able to pass native JS objects / arrays into the model. The default integration of the Rhino JavaScript engine into web scripts should ensure that data put into model is converted from JS to Java (and if necessary + supported to specific FTL types).

Now since I don't know about the expected JSON structure that you parse I can't know if some fluke error in the JSON can result in the eval() to just return a String instead of an array you expected…

Dear Axel, Thank you for your reply.

In the meantime I tried to do in javascript the operation of "manually" converting the model to javascript objects, like this:
<javascript>
var connector = remote.connect("alfresco-webscripts");
var webscriptUrl = "/mySearch?query=" + args.query;
var response = connector.get(webscriptUrl);
var data = eval(response);
logger.log(data);
var results = [];
for (var i=0,n=data.length; i<n; i++) {
  var item = data;
  results.push({"id":item.id, "name":item.name, "title":item.title});
}
model.results = results;
</javascript>

But it was not working –> the results were an empty array, because data.length was undefined.

Later I dropped the eval call and replaced it with jsonUtils.toObject(data)
But then I started getting errors like: org.mozilla.javascript.EcmaError: TypeError: Cannot find function length.

While debugging this, something not at all helpful (and one of the most annoying) was the fact that I cannot perform operations like:
<javascript>
var a = jsonUtils.toObject('{"a":123}');
logger.log(a);  // ==> org.mozilla.javascript.EcmaError: TypeError: Cannot find default value for object.
logger.log(typeof a);  // ==> "object"   What kind of object?? What fields and methods does it have??
</javascript>

In the end I removed the "manual" re-assignment, and now it's working with just this:
<javascript>
var connector = remote.connect("alfresco-webscripts");
var webscriptUrl = "/mySearch?query=" + args.query;
var response = connector.get(webscriptUrl);
var data = jsonUtils.toObject(response);
model.results = data.results;
</javascript>

Note that in order to make it work I had to also change the FTL for my AlfrescoRepo data-webscript, which was returning a JSON Array:
<javascript>
[
{"id":"1","name":"John","Title":"Johnny"},
{"id":"2","name":"Jane","Title":"Jenny"},
]
</javascript>

I was getting the following error from jsonUtils.toObject(response): org.json.simple.JSONArray cannot be cast to org.json.simple.JSONObject

I had to change it to return a JSON Object, like this:
<javascript>
{"results":[
{"id":"1","name":"John","Title":"Johnny"},
{"id":"2","name":"Jane","Title":"Jenny"},
]}
</javascript>

risharommi
Champ in-the-making
Champ in-the-making
Really amazing and impressive sharing thanks

web design service
Welcome to the new Hyland Connect. Get started or submit feedback.