diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.desc.xml
new file mode 100644
index 0000000000..9482cd1735
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.desc.xml
@@ -0,0 +1,9 @@
+
+ Form
+ Get a form to view or edit the metadata of a given node.
+ /api/forms/node/{store_type}/{store_id}/{id}
+ /api/forms/node/{path}
+
+ user
+ required
+
\ No newline at end of file
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.js b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.js
new file mode 100644
index 0000000000..bf8d698d25
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.js
@@ -0,0 +1,104 @@
+function main()
+{
+ // Extract template args
+ var ta_storeType = url.templateArgs['store_type'];
+ var ta_storeId = url.templateArgs['store_id'];
+ var ta_id = url.templateArgs['id'];
+ var ta_path = url.templateArgs['path'];
+
+ logger.log("ta_storeType = " + ta_storeType);
+ logger.log("ta_storeId = " + ta_storeId);
+ logger.log("ta_id = " + ta_id);
+ logger.log("ta_path = " + ta_path);
+
+ var formUrl = '';
+ // The template argument 'path' only appears in the second URI template.
+ if (ta_path != null)
+ {
+ //TODO Need to test this path.
+ formUrl = ta_path;
+ }
+ else
+ {
+ formUrl = ta_storeType + '://' + ta_storeId + '/' + ta_id;
+ }
+ logger.log("formUrl = " + formUrl);
+
+ var formScriptObj = formService.getForm(formUrl);
+
+ if (formScriptObj == null)
+ {
+ var message = "Form " + formUrl + " not found.";
+ logger.log(message);
+ status.setCode(404, message);
+ return;
+ }
+
+ var formModel = {};
+ formModel.data = {};
+
+ formModel.data.item = '/api/node/' + ta_storeType + '/' + ta_storeId + '/' + ta_id;
+ formModel.data.submissionUrl = '/api/forms/node/' + ta_storeType + '/' + ta_storeId + '/' + ta_id;
+ formModel.data.type = formScriptObj.type;
+
+ formModel.data.definition = {};
+ formModel.data.definition.fields = {};
+ for (var fieldName in formScriptObj.fieldDefinitionData)
+ {
+ // We're explicitly listing the object fields of FieldDefinition.java and its
+ // subclasses here.
+ // I don't see a way to get these dynamically at runtime.
+ var supportedBaseFieldNames = ['name', 'label', 'description', 'binding',
+ 'defaultValue', 'group', 'protectedField'];
+ var supportedPropertyFieldNames = ['dataType', 'mandatory',
+ 'repeats', 'constraints'];
+ var supportedAssociationFieldNames = ['endpointType', 'endpointDirection',
+ 'endpointMandatory', 'endpointMany'];
+
+ var allSupportedFieldNames = supportedBaseFieldNames
+ .concat(supportedPropertyFieldNames)
+ .concat(supportedAssociationFieldNames);
+
+ formModel.data.definition.fields[fieldName] = {};
+ for (var i = 0; i < allSupportedFieldNames.length; i++) {
+ var nextSupportedName = allSupportedFieldNames[i];
+ var nextValue = formScriptObj.fieldDefinitionData[fieldName][nextSupportedName];
+
+ if (nextValue != null) {
+ formModel.data.definition.fields[fieldName][nextSupportedName] = nextValue;
+ }
+ }
+
+ // Special handling for the 'type' property
+ // For now, this can have a value of 'property' or 'association'
+
+ //TODO Temporary impl here.
+ if (formModel.data.definition.fields[fieldName]['dataType'] != null)
+ {
+ formModel.data.definition.fields[fieldName]['type'] = 'property';
+ }
+ else
+ {
+ formModel.data.definition.fields[fieldName]['type'] = 'association';
+ }
+ }
+
+ formModel.data.formData = {};
+ for (var k in formScriptObj.formData.data)
+ {
+ var value = formScriptObj.formData.data[k].value;
+
+ if (value instanceof java.util.Date)
+ {
+ formModel.data.formData[k] = utils.toISO8601(value);
+ }
+ else
+ {
+ formModel.data.formData[k] = value;
+ }
+ }
+
+ model.form = formModel;
+}
+
+main();
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.json.ftl
new file mode 100644
index 0000000000..2a6c0c7243
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.get.json.ftl
@@ -0,0 +1,2 @@
+<#import "form.lib.ftl" as formLib/>
+<@formLib.formJSON form=form/>
\ No newline at end of file
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.lib.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.lib.ftl
new file mode 100644
index 0000000000..03b4a0b80f
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.lib.ftl
@@ -0,0 +1,62 @@
+<#macro formJSON form>
+ <#escape x as jsonUtils.encodeJSONString(x)>
+{
+ "data" :
+ {
+ "item" : "${form.data.item}",
+ "submissionUrl" : "${form.data.submissionUrl}",
+ "type" : "${form.data.type}",
+ "definition" :
+ {
+ "fields" :
+ [
+ <#list form.data.definition.fields?keys as k>
+ {
+ <#list form.data.definition.fields[k]?keys as c>
+ <#if form.data.definition.fields[k][c]?is_boolean>
+ "${c}" : ${form.data.definition.fields[k][c]?string}<#if c_has_next>,#if>
+ <#elseif form.data.definition.fields[k][c]?is_sequence>
+ "${c}" :
+ [{
+ <#list form.data.definition.fields[k][c] as q>
+ "type" : "${q.type}"<#if q.params?exists>,
+ "params" : {
+ <#list q.params?keys as p>
+ <#-- Render booleans without the inverted commas -->
+
+ <#-- Can I create a macro for boolean rendering? -->
+
+ <#if q.params[p]?is_boolean>
+ "${p}" : ${q.params[p]}<#if p_has_next>,#if>
+ <#else>
+ "${p}" : "${q.params[p]}"<#if p_has_next>,#if>
+ #if>
+ #list>
+ }
+ #if>
+ #list>
+ }]<#if c_has_next>,#if>
+ <#else>
+ "${c}" : "${form.data.definition.fields[k][c]}"<#if c_has_next>,#if>
+ #if>
+ #list>
+ }<#if k_has_next>,#if>
+ #list>
+ ]
+ },
+ "formData" :
+ {
+ <#list form.data.formData?keys as k>
+ <#if form.data.formData[k]?is_boolean>
+ <#-- Render boolean data without the surrounding inverted commas -->
+ "${k}" : ${form.data.formData[k]?string}<#if k_has_next>,#if>
+ <#else>
+ <#-- All other data rendered with inverted commas -->
+ "${k}" : "${form.data.formData[k]}"<#if k_has_next>,#if>
+ #if>
+ #list>
+ }
+ }
+}
+ #escape>
+#macro>
\ No newline at end of file
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.desc.xml
new file mode 100644
index 0000000000..5754f7f34d
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.desc.xml
@@ -0,0 +1,8 @@
+
+ Form
+ Handles the submission of a form
+ /api/forms/node/{store_type}/{store_id}/{id}
+ /api/forms/node/{path}
+ user
+ required
+
\ No newline at end of file
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.html.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.html.ftl
new file mode 100644
index 0000000000..7c877adaaf
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.html.ftl
@@ -0,0 +1,15 @@
+
+
+ Form Posted
+
+
+ ${message}
+ <#if data.fields?exists>
+
+ <#list data.fields as field>
+ - ${field.name} = ${field.value}
+ #list>
+
+ #if>
+
+
\ No newline at end of file
diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.js b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.js
new file mode 100644
index 0000000000..117024cbb4
--- /dev/null
+++ b/config/alfresco/templates/webscripts/org/alfresco/repository/forms/form.post.js
@@ -0,0 +1,43 @@
+function main()
+{
+ var ta_storeType = url.templateArgs['store_type'];
+ var ta_storeId = url.templateArgs['store_id'];
+ var ta_id = url.templateArgs['id'];
+ var ta_mode = url.templateArgs['mode'];
+ var ta_path = url.templateArgs['path'];
+
+ var nodeRef = '';
+ // The template argument 'path' only appears in the second URI template.
+ if (ta_path != null)
+ {
+ //TODO Need to test this path.
+ nodeRef = ta_path;
+ }
+ else
+ {
+ nodeRef = ta_storeType + '://' + ta_storeId + '/' + ta_id;
+ }
+
+ logger.log("POST request received for nodeRef: " + nodeRef);
+
+ // TODO: check the given nodeRef is real
+
+ // persist the submitted data using the most appropriate data set
+ if (typeof formdata !== "undefined")
+ {
+ model.data = formdata;
+ formService.saveForm(nodeRef, formdata);
+ }
+ else if (typeof json !== "undefined")
+ {
+ formService.saveForm(nodeRef, json);
+ }
+ else
+ {
+ formService.saveForm(nodeRef, args);
+ }
+
+ model.message = "Successfully updated node " + nodeRef;
+}
+
+main();
\ No newline at end of file
diff --git a/source/java/org/alfresco/repo/web/scripts/forms/TestFormRestAPI.java b/source/java/org/alfresco/repo/web/scripts/forms/TestFormRestAPI.java
new file mode 100644
index 0000000000..e423c84838
--- /dev/null
+++ b/source/java/org/alfresco/repo/web/scripts/forms/TestFormRestAPI.java
@@ -0,0 +1,202 @@
+package org.alfresco.repo.web.scripts.forms;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.content.MimetypeMap;
+import org.alfresco.repo.model.Repository;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.web.scripts.BaseWebScriptTest;
+import org.alfresco.repo.web.scripts.thumbnail.ThumbnailServiceTest;
+import org.alfresco.service.cmr.model.FileFolderService;
+import org.alfresco.service.cmr.model.FileInfo;
+import org.alfresco.service.cmr.repository.ContentService;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.GUID;
+import org.alfresco.web.scripts.TestWebScriptServer.GetRequest;
+import org.alfresco.web.scripts.TestWebScriptServer.Response;
+import org.alfresco.web.scripts.json.JSONUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+public class TestFormRestAPI extends BaseWebScriptTest {
+ private FileFolderService fileFolderService;
+ private ContentService contentService;
+ private NodeService nodeService;
+ private Repository repositoryHelper;
+ private Response response;
+ private String jsonResponseString;
+ private NodeRef testRoot;
+ private NodeRef testPdfNode;
+ private String pathToTestPdfNode;
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+ this.fileFolderService = (FileFolderService)getServer().getApplicationContext().getBean("FileFolderService");
+ this.contentService = (ContentService)getServer().getApplicationContext().getBean("ContentService");
+ this.repositoryHelper = (Repository)getServer().getApplicationContext().getBean("repositoryHelper");
+ this.nodeService = (NodeService)getServer().getApplicationContext().getBean("NodeService");
+
+ AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName());
+
+ this.testRoot = this.repositoryHelper.getCompanyHome();
+
+ // Create a dummy node purely for test purposes.
+ InputStream pdfStream = ThumbnailServiceTest.class.getClassLoader().getResourceAsStream("org/alfresco/repo/web/scripts/forms/test_doc.pdf");
+ assertNotNull(pdfStream);
+
+ String guid = GUID.generate();
+
+ FileInfo fileInfoPdf = this.fileFolderService.create(this.testRoot, "test_forms_doc" + guid + ".pdf", ContentModel.TYPE_CONTENT);
+ this.testPdfNode = fileInfoPdf.getNodeRef();
+
+ // Add an aspect.
+ Map aspectProps = new HashMap(2);
+ aspectProps.put(ContentModel.PROP_TITLE, "Test form title");
+ aspectProps.put(ContentModel.PROP_DESCRIPTION, "Test form description");
+ nodeService.addAspect(testPdfNode, ContentModel.ASPECT_TITLED, aspectProps);
+
+ ContentWriter contentWriter = this.contentService.getWriter(fileInfoPdf.getNodeRef(), ContentModel.PROP_CONTENT, true);
+ contentWriter.setEncoding("UTF-8");
+ contentWriter.setMimetype(MimetypeMap.MIMETYPE_PDF);
+ contentWriter.putContent(pdfStream);
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("/api/forms/node/workspace/")
+ .append(testPdfNode.getStoreRef().getIdentifier())
+ .append("/")
+ .append(testPdfNode.getId());
+ this.pathToTestPdfNode = builder.toString();
+ }
+
+ //TODO Add a tearDown which deletes the temporary pdf file above.
+
+ public void testResponseContentType() throws Exception
+ {
+ sendGetReqAndInitRspData(pathToTestPdfNode, 200);
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+
+ //TODO Remove this.
+ System.out.println(jsonResponseString);
+ }
+
+ //TODO Perhaps separate positive and negative test cases into two JUnit classes.
+ public void testGetFormForNonExistentNode() throws Exception
+ {
+ sendGetReqAndInitRspData(pathToTestPdfNode.replaceAll("\\d", "x"), 404);
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+ }
+
+ public void testJsonContentParsesCorrectly() throws Exception
+ {
+ sendGetReqAndInitRspData(pathToTestPdfNode, 200);
+
+ Object jsonObject = new JSONUtils().toObject(jsonResponseString);
+ assertNotNull("JSON object was null.", jsonObject);
+ }
+
+ public void testJsonUpperStructure() throws Exception
+ {
+ sendGetReqAndInitRspData(pathToTestPdfNode, 200);
+
+ JSONObject jsonParsedObject = new JSONObject(new JSONTokener(jsonResponseString));
+ assertNotNull(jsonParsedObject);
+
+ Object dataObj = jsonParsedObject.get("data");
+ assertEquals(JSONObject.class, dataObj.getClass());
+ JSONObject rootDataObject = (JSONObject)dataObj;
+
+ assertEquals(5, rootDataObject.length());
+ String item = (String)rootDataObject.get("item");
+ String submissionUrl = (String)rootDataObject.get("submissionUrl");
+ String type = (String)rootDataObject.get("type");
+ JSONObject definitionObject = (JSONObject)rootDataObject.get("definition");
+ JSONObject formDataObject = (JSONObject)rootDataObject.get("formData");
+
+ assertNotNull(item);
+ assertNotNull(submissionUrl);
+ assertNotNull(type);
+ assertNotNull(definitionObject);
+ assertNotNull(formDataObject);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testJsonFormData() throws Exception
+ {
+ sendGetReqAndInitRspData(pathToTestPdfNode, 200);
+
+ JSONObject jsonParsedObject = new JSONObject(new JSONTokener(jsonResponseString));
+ assertNotNull(jsonParsedObject);
+
+ JSONObject rootDataObject = (JSONObject)jsonParsedObject.get("data");
+
+ JSONObject formDataObject = (JSONObject)rootDataObject.get("formData");
+ List keys = new ArrayList();
+ for (Iterator iter = formDataObject.keys(); iter.hasNext(); )
+ {
+ keys.add((String)iter.next());
+ }
+ // Threshold is a rather arbitrary number. I simply want to ensure that there
+ // are *some* entries in the formData hash.
+ final int threshold = 5;
+ int actualKeyCount = keys.size();
+ assertTrue("Expected more than " + threshold +
+ " entries in formData. Actual: " + actualKeyCount, actualKeyCount > threshold);
+ }
+
+ public void testJsonDefinitionFields() throws Exception
+ {
+ sendGetReqAndInitRspData(pathToTestPdfNode, 200);
+
+ JSONObject jsonParsedObject = new JSONObject(new JSONTokener(jsonResponseString));
+ assertNotNull(jsonParsedObject);
+
+ JSONObject rootDataObject = (JSONObject)jsonParsedObject.get("data");
+
+ JSONObject definitionObject = (JSONObject)rootDataObject.get("definition");
+
+ JSONArray fieldsArray = (JSONArray)definitionObject.get("fields");
+
+ //TODO This will all be revamped when I introduce test code based on a known
+ // node. But in the meantime, I'll keep it general.
+ for (int i = 0; i < fieldsArray.length(); i++)
+ {
+ Object nextObj = fieldsArray.get(i);
+
+ JSONObject nextJsonObject = (JSONObject)nextObj;
+ List fieldKeys = new ArrayList();
+ for (Iterator iter2 = nextJsonObject.keys(); iter2.hasNext(); )
+ {
+ fieldKeys.add((String)iter2.next());
+ }
+ for (String s : fieldKeys)
+ {
+ if (s.equals("mandatory") || s.equals("protectedField"))
+ {
+ assertEquals("JSON booleans should be actual booleans.", java.lang.Boolean.class, nextJsonObject.get(s).getClass());
+ }
+ }
+ }
+ }
+
+ private void sendGetReqAndInitRspData(String url, int expectedStatusCode) throws IOException,
+ UnsupportedEncodingException
+ {
+ response = sendRequest(new GetRequest(url), expectedStatusCode);
+ jsonResponseString = response.getContentAsString();
+ }
+}
diff --git a/source/java/org/alfresco/repo/web/scripts/forms/test_doc.pdf b/source/java/org/alfresco/repo/web/scripts/forms/test_doc.pdf
new file mode 100644
index 0000000000..9b033d052b
Binary files /dev/null and b/source/java/org/alfresco/repo/web/scripts/forms/test_doc.pdf differ