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>, + <#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>, + <#else> + "${p}" : "${q.params[p]}"<#if p_has_next>, + + + } + + + }]<#if c_has_next>, + <#else> + "${c}" : "${form.data.definition.fields[k][c]}"<#if c_has_next>, + + + }<#if k_has_next>, + + ] + }, + "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>, + <#else> + <#-- All other data rendered with inverted commas --> + "${k}" : "${form.data.formData[k]}"<#if k_has_next>, + + + } + } +} + + \ 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> + + + + \ 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