Information | Repository web scripts |
---|---|
Support Status | Full Support [1] |
Architecture Information | |
Description | Web Scripts are a way of implementing REST-based API [4]. They could also be referred
to as Web Services. They are stateless and scale extremely well. Repository Web
Scripts are defined in XML, JavaScript, and FreeMarker files and stored under
alfresco/extension/templates/webscripts. Repository Web
Scripts are referred to as Data Web Scripts as they usually return JSON or
XML. Before embarking on implementing a Repository web scripts it is recommended that you establish if the required functionality is already available out-of-the-box. Many operations that you might want to perform may be available, see Alfresco REST API [5]. The simplest Web Script you can write consists of a descriptor and a template. The descriptor will tell you what URL that should be used to invoke the Web Script. The template is used to assemble the output returned from the Web Script. This kind of Web Script is very static to its nature and will always return the exact same content. Most Web Scripts also include a controller that is used to dynamically assemble a map of data that is then processed by the template to produce the final output. The data that the controller produces could come from anywhere as the controller can be implemented in both JavaScript and Java. Any content from the repository that should be included in the response can be fetched via Alfresco Community Edition-specific JavaScript root objects, such as companyhome, or services, such as the Node Service, if the controller is implemented in Java. The following picture illustrates how a Repository Web Script request is processed:
The controller can fetch content from different sources, such as the repository, or a remote Web Service on the Internet. Note that the special root object called remote [6], which is available for Surf web scripts to fetch remote data on the internet, is not available when implementing a Repository Web Script JavaScript controller. To fetch remote data on the Internet from a Repository Web Script, a Java controller is needed. Now, to get going implementing web scripts we will start with the simplest possible Repository Web Script. The usual Hello World example comes to mind. When implementing a new Web Script it is good to start with the descriptor file, it will define what URL(s) that should be used to invoke the Web Script. It is defined in XML and looks something like this: <webscript> <shortname>Hello World</shortname> <description>Hello World Sample Web Script that responds back with a greeting</description> <url>/tutorial/helloworld</url> <format default="html"></format> <family>Alfresco Tutorials</family> </webscript> The important part here is the <url> element, which determines what URL should be used to invoke the Web Script. When specifying the URL leave out the part that maps to the Web Script dispatcher Servlet, which is http://{host}:{port}/alfresco/service. So to invoke this Web Script use a URL with the http://{host}:{port}/alfresco/service/tutorial/helloworld format. Next important thing in the descriptor file is the <format element, which specifies what content format we can expect in the response when invoking this Web Script. In this case it will return a HTML fragment, so we set format to default="html". Finally we need to somehow define a unique identifier for this Web Script, which will be used to look up other files that are part of the Web Script implementation. This is handled implicitly by the file name convention, which for Web Script descriptor files follow the <web script id>.<http method>.desc.xml format. If we store this descriptor in a file called helloworld.get.desc.xml then the unique identifier will be helloworld. But that's not all, the HTTP method also plays a part in the identification of a Web Script, in this case it is set to get, which means it is intended to be invoked with a HTTP GET Request. Important: The Web Script URL needs to be unique throughout the Alfresco Community Edition
installation. And if two or more web scripts have the same identifier, then
they need to be stored in different directory locations. For example, if you
have two extensions deploying a Web Script with the same file name, in the same
location (i.e. directory), then the last one to be deployed will overwrite the
other one, even if the URL is different between the two.
To complete the Hello World Web Script implementation we just need a template to go along with the descriptor, it is defined in a FreeMarker file and looks like this: <h2>Hello World!</h2> Web Script template file names also follow a naming convention: <web script id>.<http method>.<format>.ftl. The above template could be stored in a file called helloworld.get.html.ftl, which would implicitly associate it with the descriptor file as it has the same identifier and HTTP method. We are also indicating that this template produces HTML markup. This Web Script implementation is now complete. To try out the Hello World Web Script we first need to deploy it by copying the files to the correct directory in the Alfresco Community Edition installation, see below for locations. Then refresh the web scripts from the http://{host}:{port}/alfresco/service/index page so Alfresco Community Edition knows about it. And then invoke it using the URL in a browser as follows:
Most of the time the content that is returned is provided indirectly via a controller. The controller sets up the model containing the data that should be passed over to the template. Controllers can be implemented in both JavaScript (this is server side JavaScript, Alfresco Community Edition provides this by embedding the Rhino JavaScript engine) and Java. Let's add a JavaScript controller for the Hello World Web Script. It will put a property called message in the model. This new property will contain a slightly improved Hello World message that includes the name of the logged in user. Here is the controller implementation: model.message = "Hello World " + person.properties.firstName + ' ' + person.properties.lastName + "!"; Here we use an Alfresco Community Edition-specific JavaScript root object called person to get first and last name of the logged in user. The model variable is automatically available to us in the controller and we can put whatever data we want in it for later use in the template. The Web Script controller file names follow the <web script id>.<http method>.js naming convention. The above controller should be stored in a file called helloworld.get.js so it is matched up with the Hello World Web Script descriptor. To take advantage of this new data in the model we need to update the template as follows: <h2>${message}</h2> The update to the Web Script is now finished. However, if we were to try and invoke the Web Script we would see an exception as currently it is not set up to authenticate with a username and password. We cannot use the people root object to access Repository information about users without being authenticated. In fact, we cannot access anything in the Repository without first authenticating, so using other root objects such as companyhome requires authentication too. Authentication is configured in the descriptor file with an extra <authentication> element as follows: <webscript> <shortname>Hello World</shortname> <description>Hello World Sample Web Script that responds back with a greeting</description> <url>/tutorial/helloworld</url> <format default="html"></format> <authentication>user</authentication> <family>Alfresco Tutorials</family> </webscript> When setting the authentication property to be able to read and write to the Repository we need to have these operations wrapped in a transaction. This is automatically done as soon as we set the authentication element to anything else than none. By default another element called <transaction> is then set to required. After deploying the updated Web Script files and the new controller file, and refreshing the web scripts, we will see the following when invoking it again (assuming we logged in as Administrator):
Now, what if we wanted to present the Hello World message in different languages depending on what the browser Accept-Language header was, how would we do that? We would then turn to Web Script i18n properties files. These files are created in the same way as Java resource bundles. The naming convention for these files is <web script id>.<http method>[_<locale>].properties. For the default English resource file you can leave out the locale. So for our Hello World Web Script it would be called helloworld.get.properties and contain the following: hello.world=Hello World To add a Swedish translation we would create a properties file called helloworld.get_sv.properties with the following content: hello.world=Hej Världen To make use of this property we would have to update the controller as follows: model.message = person.properties.firstName + ' ' + person.properties.lastName + "!"; Leaving out the Hello World string so it can be localized. The template need the following update to read the resource string: <h2>${msg("hello.world")} - ${personName}</h2> There are also situations where we just want to be able to externally configure the Web Script with minimal changes to the main implementation of it. Basically we don't want to touch the descriptor, controller, or template. Just feed it with some new configuration. Let's say for example that our greeting message should be slightly different at certain times of the year, such as an extra Merry Christmas message around that time. This can be done with an extra configuration file that follows the <web script id>.<http method>.config.xml naming convention. The Hello World Web Script configuration will look like this: <greeting> <text>Merry Christmas!</text> <active>true</active> </greeting> The configuration file can contain any arbitrary XML structure. In this case it contains a message text and an indication if this text should be active or not. We store this configuration in a file called helloworld.get.config.xml. To access this configuration we would have to make a change to the controller as follows: var greeting = new XML(config.script); model.greetingActive = greeting.active[0].toString(); model.greetingText = greeting.text[0].toString(); model.personName = person.properties.firstName + ' ' + person.properties.lastName + "!"; We use the JavaScript root object config to access the XML. This is then fed into the XML object, which is part of the E4X JavaScript library that enables us to process XML directly in JavaScript (more info: http://www.w3schools.com/e4x/default.asp). We can then navigate into the XML structure and grab the data that we need. We add two variables to the model to hold the greeting message and if it should be active or not. All we got to do now is update the template to take advantage of the new data: <h2>${msg("hello.world")} - ${personName}</h2> <#if greetingActive == "true"> <p> <i>${greetingText}</i> </p> </#if> This is the first time we have started to use some FreeMarker directives. Common statements such as if,then,else are supported. Directives are preceded with #. Note that when you use model variables such as the greetingActive inside a directive statement they don't have to be enclosed in ${ }. Invoking the Hello World Web Script should now give us the following result:
It is now very simple to change the extra message to whatever we want without having to touch the main implementation of the Web Script, just update the helloworld.get.config.xml file, and we can turn off the message all together if we want to. Sometimes when implementing a Web Script there are things that cannot be done in a JavaScript controller, such as accessing the file system and fetching content on the Internet. We then need to turn to Java based controllers. To implement a Web Script controller in Java we create a class that extends the org.springframework.extensions.webscripts.DeclarativeWebScript class. Using a Java controller will allow us to fetch and process data from wherever we want to. Let's implement a Java controller that just adds a current date and time variable to the model: package org.alfresco.tutorial.webscripts; import org.springframework.extensions.webscripts.Cache; import org.springframework.extensions.webscripts.DeclarativeWebScript; import org.springframework.extensions.webscripts.Status; import org.springframework.extensions.webscripts.WebScriptRequest; import java.util.Date; import java.util.HashMap; import java.util.Map; public class HelloWorldWebScript extends DeclarativeWebScript { @Override protected Map<String, Object> executeImpl( WebScriptRequest req, Status status, Cache cache) { Map<String, Object> model = new HashMap<String, Object>(); model.put("currentDateTime", new Date()); return model; } } Note here that we are expected to return a model object, which is just a hash map. When we got both a JavaScript controller and a Java controller the latter one is executed first. The new Java controller is not yet associated with the Hello World Web Script. We need to define a Spring bean for it with an id that connects the controller with this Web Script: <beans> <bean id="webscript.alfresco.tutorials.helloworld.get" class="org.alfresco.tutorial.webscripts.HelloWorldWebScript" parent="webscript"> </bean> </beans> The id should be specified following the webscript.<packageId>.<web-script-id>.<httpMethod> format. The trickiest part of the id is probably the packageId. When specified as in the above example it is assumed that the descriptor file is located in the alfresco/extension/templates/webscripts/alfresco/tutorials directory. With the new currentDateTime variable in the model we can use it in the template to get it displayed in the response: <#assign datetimeformat="yyyy-MM-dd HH:mm:ss"> <h2>${msg("hello.world")} - ${personName}</h2> <#if greetingActive == "true"> <p> <i>${greetingText}</i> </p> </#if> <p>The time is now: "${currentDateTime?string(datetimeformat)}</p> Here we use another FreeMarker directive called assign that can be used to define new variables. In this case we define a new variable datetimeformat to hold the date and time format we want to use when displaying current date and time. To display the date in this format we use a so called built-in for dates called string. Calling the Web Script will now show the following response:
Important: The DeclarativeWebScript class is used when
we have a template, and maybe a JavaScript controller as part of the Web Script.
But there are situations, such as streaming and downloading a file, where there is
no need for a template. In these cases we can extend the
org.springframework.extensions.webscripts.AbstractWebScript
class instead. It has an execute method that will allow you to return nothing and
instead just put something on the response output stream, as in the following
example:
package org.alfresco.tutorial.webscripts; import org.json.JSONException; import org.json.JSONObject; import org.springframework.extensions.webscripts.AbstractWebScript; import org.springframework.extensions.webscripts.WebScriptException; import org.springframework.extensions.webscripts.WebScriptRequest; import org.springframework.extensions.webscripts.WebScriptResponse; import java.io.IOException; public class JSONResponseWebScript extends AbstractWebScript { @Override public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException { try { JSONObject obj = new JSONObject(); obj.put("name", "Alfresco"); String jsonString = obj.toString(); res.getWriter().write(jsonString); } catch (JSONException e) { throw new WebScriptException("Unable to serialize JSON"); } } } The Hello World Web Script demonstrates most of the features available to us when implementing web scripts. However, it might not be the most realistic Web Script implementation, it is not something we would need to do in a "real" project. It is more likely that we will have to implement a REST API based on a custom content model, such as the ACME sample content model [7]. The key principles of REST involve separating your API into logical resources. These resources are manipulated using HTTP requests where the method (GET, POST, PUT, DELETE) has specific meaning. When working with custom content models, what can we make a resource? Normally, these should be nouns that make sense from the perspective of the API consumer. We should not have internal implementation details visible in our API! When looking at a content model it probably makes sense to use the types as resources, so for the ACME content model we could have the ACME Document, ACME Contract, and so on as resources. When we have identified our resources, we need to identify what actions apply to them and how those would map to the API. REST-ful principles provide strategies to handle CRUD actions using HTTP methods mapped as follows:
A good thing about REST is that we leverage existing HTTP methods to implement significant functionality on just a single /acmedocs endpoint. There are no method naming conventions to follow and the URL structure is clean and clear. Try and keep the resource URLs as lean as possible. Things like filters, sorting, search, and what properties to return can quite easily be implemented as parameters on top of the base URL. Here are some examples:
When it comes to response format JSON is usually a good choice as it is compact and works well with most programming languages and widget libraries. As a demonstration on how to implement a REST API according to these best practices, we will look at how to implement a Web Script that can be used to return a list of ACME documents matching a keyword using Full Text Search (FTS). Based on REST API design principles, the descriptor would then look something like this: <webscript> <shortname>Search ACME Documents</shortname> <description>Returns metadata as JSON for all ACME documents in the repository that matches search keyword</description> <url>/tutorial/acmedocs?q={keyword}</url> <authentication>user</authentication> <format default="json"></format> <family>Alfresco Tutorials</family> </webscript> The above descriptor could be stored in a file called acme-documents.get.desc.xml as this Web Script should be used to search for files with the ACME document type applied. To invoke this Web Script we would use a URL with the format http://{host}:{port}/alfresco/service/tutorial/acmedocs?q=london. Next step is to create a controller that takes the search keyword, does a FTS, and then adds information about the matching nodes to the model object: function AcmeDocumentInfo(doc) { this.name = doc.name; this.creator = doc.properties.creator; this.createdDate = doc.properties.created; this.modifier = doc.properties.modifier; this.modifiedDate = doc.properties.modified; this.docId = doc.properties["acme:documentId"]; this.securityClassification = doc.properties["acme:securityClassification"]; } function main() { var searchKeyword = args["q"]; if (searchKeyword == null || searchKeyword.length == 0) { searchKeyword = ""; } else { searchKeyword = " AND TEXT:\"" + searchKeyword + "\""; } var acmeDocNodes = search.luceneSearch("TYPE:\"acme:document\"" + searchKeyword); if (acmeDocNodes == null || acmeDocNodes.length == 0) { status.code = 404; status.message = "No ACME documents found"; status.redirect = true; } else { var acmeDocInfos = new Array(); for (i = 0; i < acmeDocNodes.length; i++) { acmeDocInfos[i] = new AcmeDocumentInfo(acmeDocNodes[i]); } model.acmeDocs = acmeDocInfos; return model; } } main(); Here we first check if we got a search keyword passed in, if we don't we will exclude the FTS from the query. We then do the Lucene search on the ACME Document type and keyword using the Alfresco Community Edition-specific search root object. If we get any nodes back we create an array of information objects that we add to the model to be sent to the template. If the query did not match any nodes we use the special status root object to send back a HTTP 404 not found message. The controller needs to be stored in a file called acme-documents.get.js to match up with the descriptor. The template for this Web Script should construct a JSON representation of the resources/nodes that match the query: <#assign datetimeformat="EEE, dd MMM yyyy HH:mm:ss zzz"> { "acmeDocs" : [ <#list acmeDocs as acmeDoc> { "name" : "${acmeDoc.name}", "creator" : "${acmeDoc.creator}", "createdDate" : "${acmeDoc.createdDate?string(datetimeformat)}", "modifier" : "${acmeDoc.modifier}", "modifiedDate" : "${acmeDoc.modifiedDate?string(datetimeformat)}", "docId" : "${acmeDoc.docId!"Unknown"}", "securityClass" : "${acmeDoc.securityClassification!"Unknown"}" } <#if acmeDoc_has_next>,</#if> </#list> ] } Here a new FreeMarker directive called list is used to loop through the document information for the matching nodes. We also use a very handy build-in (!) that will check if the variable has a value (i.e. is not null), if it doesn't the right hand side value will be used as default. The template should be stored in a file called acme-documents.get.json.ftl as it returns JSON and should be matched up with the correct descriptor. This completes this ACME Docs Web Script, executing it will return a result looking something like this:
In this call there were two documents that matched, having the ACME Document type
applied, or a sub-type such as ACME Contract, and with a text that contained the
word "sample".
We have now seen a lot of examples of how to get stuff from the repository, what about if we wanted to POST some stuff to the repository and store it? This is simple, tell the web script container that the web script is of type POST, and that we expect to upload and store stuff in the repository with it. As an example, let's create an ACME Docs web script that can be used to upload some JSON data with information that is to be used when creating an ACME Text document. The descriptor will look like this: <webscript> <shortname>Create ACME Document</shortname> <description>Create an ACME Text Document by uploading JSON data with both metadata and content for the text document. POST body should include JSON such as: { name: "acmedocument2.txt", docId: "DOC002", securityClass: "Public", content: "Some text to represent the content of the document" } </description> <url>/tutorial/acmedocs</url> <authentication>user</authentication> <transaction>required</transaction> <format default="html">any</format> <family>Alfresco Tutorials</family> </webscript> The above descriptor could be stored in a file called acme-documents.post.desc.xml as this Web Script should be used to POST stuff to the Repository. To invoke this Web Script we would use a cURL command looking something like this: curl -v -u admin:admin -d @sample.json -H 'Content-Type:application/json' http://localhost:8080/alfresco/service/tutorial/acmedocs The sample.json file would contain the JSON structure as described in the descriptor. Next up is the controller, which should extract the JSON and then create the ACME Text Document based on the data: // Get the POSTed JSON data var name = json.get("name"); var docId = json.get("docId"); var securityClass = json.get("securityClass"); var content = json.get("content"); // Create the new ACME Text Document var acmeTextDocFileName = name; var guestHomeFolder = companyhome.childByNamePath("Guest Home"); var acmeTextDocFile = guestHomeFolder.childByNamePath(acmeTextDocFileName); if (acmeTextDocFile == null) { var contentType = "acme:document"; var properties = new Array(); properties['acme:documentId'] = docId; properties['acme:securityClassification'] = securityClass; acmeTextDocFile = guestHomeFolder.createNode(acmeTextDocFileName, contentType, properties); acmeTextDocFile.content = content; acmeTextDocFile.mimetype = "text/plain"; // Send back the NodeRef so it can be further used if necessary model.nodeRef = acmeTextDocFile.nodeRef; } else { status.code = 404; status.message = "ACME Text Document with name: '" + acmeTextDocFileName + "' already exist!"; status.redirect = true; } The controller file should be called acme-documents.post.json.js to tell the Web Script container that it will be receiving POSTed JSON. When the controller is expecting JSON like this it provides a convenience root object called json that can be used to extract the JSON data. We then use another Alfresco Community Edition-specific root object called companyhome that can be used to search for a folder, such as /Guest Home in this case. The childByNamePath assumes that you are searching from /Company Home so no need to specify it in the path to the node, this method can also be used to search for files. The node reference for the newly created ACME Text document is passed in to the template via the model. The template for the Web Script is simple and looks like this: <p>The ACME Document was added successfully with the node reference: ${nodeRef}</p> The template file should be called acme-documents.post.html.ftl to be associated with the ACME Documents Web Script. |
Deployment - App Server |
|
Deployment All-in-One SDK project [10]. |
|
More Information |
|
Sample Code | |
Tutorials |
|
Web scripts let you implement your own RESTful API without tooling or Java knowledge, requiring only a text editor. This approach to developing an Alfresco Community Edition API means that web scripts offer many advantages over existing technologies, including ease and speed of development, and flexibility in API design. By focusing on the RESTful architectural style, web scripts let you build custom URI-identified and HTTP accessible content management web services backed by the Alfresco Community Edition server.
Web scripts provide RESTful access to content held within your repository. You can place controls on your content to manage it and provide uniform access for a wide variety of client applications and services, such as a browser, portal, search engine, or custom application. Because of the inherent distributed nature of this interface, all repositories within the enterprise can resemble one logical collection of inter-related documents (like the web), letting you apply web technologies such as caching, authentication, proxies, and negotiation to your repository resources.
You can build your own RESTful interface using lightweight scripting technologies (such as JavaScript and FreeMarker), allowing you to arbitrarily map any content in the repository to resources on the web, or you can use out-of-the-box web scripts that already encapsulate many of the mappings. The Alfresco Community Edition CMIS (Content Management Interoperability Services) AtomPub binding is implemented as a series of web scripts.
You can use web scripts for various solutions, such as:
REST (Representational State Transfer) is an architectural style of which the web architecture is the most prominent example, one based on HTTP requests and responses, URIs (Uniform Resource Identifiers), and document types.
Web scripts let you implement your own RESTful API without tooling or Java knowledge. You simply need your favorite text editor. No compilation, generators, server restarts, or complex installs are required. This approach to developing an Alfresco Community Edition API means that web scripts offer many advantages over existing technologies, including ease and speed of development, and flexibility in API design.
By focusing on the RESTful architectural style and ease of development, web scripts let you build your own custom URI-identified and HTTP accessible content management web services backed by the Alfresco Community Edition server. This is like having an HTTP server with a built-in content repository allowing clients to easily access, manage, and cross-link content via a tailored RESTful interface designed specifically for the application requirements.
Data web scripts provide a server interface for client applications to query, retrieve, update, and perform processes, typically using request and response formats such as XML and JSON.
Unlike data web scripts, presentation web scripts can be hosted in the Alfresco Community Edition server or in a separate presentation server. When hosted separately, presentation web scripts in the presentation server interact with data web scripts in the Alfresco content application server by using the Repository REST API.
You can call existing web scripts or create your own web scripts for new scenarios. For example, you can create your own web script to expose a RESTful interface onto a custom content repository extension.
Users of a web script only interact through the web script interface, which comprises its URI, HTTP method, and request/response document types. All of these are described in the web script description document, which is defined by the web script creator.
A detailed reference of elements in the web script description document can be found in the Web Script Description Language Reference [50].
An example of a web script description document follows:
<webscript> <shortname>Blog Search Sample</shortname> <description>Sample that finds all blog entries whose content contains the specified search term</description> <url>/sample/blog/search?q={searchTerm}</url> <url>/sample/blog/search.atom?q={searchTerm}</url> <url>/sample/b/s?q={searchTerm}</url> <url>/sample/b/s.atom?q={searchTerm}</url> <format default="html">extension</format> <authentication>guest</authentication> <transaction>required</transaction> </webscript>
The controller script can query the repository to build a set of data items, known as a model, to render in the response. It might also update the repository for URIs that intend to modify the repository (PUT, POST, and DELETE method bindings). The JavaScript has access to the URI query string, services, and repository data entry points.
// check that search term has been provided if (args.q == undefined || args.q.length == 0) { status.code = 400; status.message = "Search term has not been provided."; status.redirect = true; } else { // perform search var nodes = search.luceneSearch("TEXT:" + args.q); model.resultset = nodes; }
The HTTP response is rendered by using one of the supplied templates, where the chosen template is based on the required response content type or status outcome. The template has access to the URI query string, common repository data entry points, and any data items built by the optional controller script.
<html> <body> <img src="${url.context}/images/logo/AlfrescoLogo32.png" alt="Alfresco" /> Blog query: ${args.q} <br/> <table> <#list resultset as node> <tr> <td><img src="${url.context}${node.icon16}"/></td> <td><a href="${url.serviceContext}/api/node/content/${node.nodeRef.storeRef.protocol}/${node.nodeRef.storeRef.identifier}/${node.nodeRef.id}/${node.name?url}">${node.name}</a></td> </tr> </#list> </table> </body> </html>
Web script description document file names have the following structure:
<web script id>.<http method>.desc.xml
The <shortname> and <description> elements provide human readable titles for the web script. You can see these in web script documentation and the web script index at: http://localhost:8080/alfresco/service/index
Controller script file names have the following structure:
<web script id>.<http method>[.<format>].js
To bind a Spring bean to a Web Script it is only necessary to create a bean with an id of the following structure:
id="webscript.<packageId>.<web script id>.<httpMethod>"
Template file names have the following structure:
<web script id>.<httpMethod>.<format>.ftl
Response status code document file names adhere to a naming convention as defined by the Web Script Framework. The appropriate response status code template is searched for in the following order:
Web Script responses may be localized. Resource file names have the following structure:
<web script id>.<httpMethod>[_<locale>].properties
Configuration is accessed via the config root object, which is available during both controller script and template execution. Configuration file names have the following structure:
<web script id>.<httpMethod>.config.xml
Placing web scripts in the classpath lets you package and deploy them with other extensions that comprise your solution. You can install them using standard Alfresco Community Edition tools without having to upload them into the repository. However, it might not be as convenient to edit them while developing them as if they were located in the Alfresco content repository where you can easily edit them using Alfresco Share. You can also export and import web scripts in the content repository using the ACP (Alfresco Content Package) mechanism.
A single Alfresco Community Edition server can contain hundreds of web scripts, each implemented with multiple files. To help manage all these web scripts, the Web Script Framework lets you organize web script component files into a hierarchical folder or package structure, similar to a Java package construct. Typically, the package name follows the reverse domain name pattern. For example, web scripts are all located in a folder named org/alfresco, which is reserved by Alfresco Community Edition.
For example:
http[s]://<host>:<port>/[<contextPath>/]/<servicePath>[/<scriptPath>] [?<scriptArgs>]
The host, port, and contextPath are all predefined by where the Alfresco Community Edition server is installed. By default, the contextPath is alfresco.
The Web Script Framework is mapped to servicePath. All Alfresco Community Edition server URL requests that start with /<contextPath>/<servicePath> trigger the Web Script Framework into action by assuming that a web script is to be invoked. By default, there are two variations of servicePath that are acceptable: /service and an abbreviated version /s.
Both of the following URIs will invoke a web script, in this case an admin call:
The scriptPath identifies the web script to invoke and is defined by the web script itself. It must be unique within an Alfresco Community Edition server. Duplicate URIs result in a web script registration failure and one of the URIs will have to be adjusted before successful registration. A scriptPath can be as simple or as complex as required and can comprise many path segments. For example, the CMIS web script URI to retrieve children of a folder residing in the repository contains the folder path. The following command line retrieves the children of the Data Dictionary folder as an Atom feed:
curl -uadmin:admin "http://localhost:8080/alfresco/s/cmis/p/Data%20Dictionary/children"
Finally, a web script URI can support query parameters as defined by the web script to control its behavior. For example, the CMIS web script to retrieve folder children can be restricted to return only documents, filtering out folders:
curl -uadmin:admin "http://localhost:8080/alfresco/s/cmis/p/Data%20Dictionary/children?types=documents"
There are some query parameters that apply to all web script invocations such as alf_ticket and format, which can be mixed with web script specific parameters:
curl -uadmin:admin "http://localhost:8080/alfresco/s/cmis/p/Data%20Dictionary/children?types=documents&format=atomfeed"
When in doubt over how to construct a URI for a given web script, consult its web script descriptor file, which you can find by using the web script index. The web script index can be displayed by directing your browser to the following URL:
http://localhost:8080/alfresco/service/index
An example of specifying a URI with two query parameters — one named "a" and the other named "b" is: /add?a={a}&b={b}
A client can generate the URI for invoking this web script when given the URI template and values for a and b. For example, if a is set to 1. and b is set to 2, the resulting URI is: /add?a=1&b=2
Query parameter tokens can indicate that the parameter is optional through the convention of appending a ‘?’ to the token name. For example, to indicate that the query parameter ‘b’ is optional, the URI template becomes: /add?a={a}&b={b?}
Although you can mark parameters as optional, it is only a convention and the Web Script Framework does not enforce mandatory query parameters. This responsibility is given to the web script developer. An example of specifying a URI path with embedded tokens — one named ‘user’ and the other named ‘profilekind’ is: /user/{user}/profile/{profilekind}
For example, the following URIs match:
/user/joe/profile/public
/user/fred/profile/full
But the following URIs do not match:
/user/profile/public
/user/joe/profile
The value of a token in a URI path can itself have multiple path segments. For example, the following URI specifies the user value joe/smith and matches the previous URI template: /user/joe/smith/profile/public
The URI /a/b invokes web script A, while the URI /a/c invokes web script B. Matching of static parts of the URI template takes precedence over matching a token value. The same token name can appear multiple times in a single URI template. Although rare, it is worth knowing the implications on matching to a web script. Consider the following URI template where the ‘user’ token is specified twice: /user/{user}/profile/{user}
For a match to occur, the value provided for each same named token must be the same.
This URI matches: /user/joe/profile/joe
But this URI does not match: /user/joe/profile/fred
Web script developers have access to the value provided for each token in both the controller script and response template.
Response status code templates have access to the same root objects as normal web script response templates, except that the default templates <code>.ftl and status.ftl only have access to the root objects url, status, server, and date.
The following scenarios can use status codes:
For example, the Folder Listing web script validates that the provided folder path actually exists in the repository using the following JavaScript in the controller script:
... if (folder == undefined || !folder.isContainer) { status.code = 404; status.message = "Folder " + folderpath + " not found."; status.redirect = true; } ...
The status root object is a special object provided to all controller scripts by the Web Script Framework. It allows a web script to specify the response status code along with an associated status message. Typically, the value of the status code is set to a standard HTTP status code.
It is useful when reporting error status codes to provide additional information about the error in the response, such as the cause of the error. To support this, the Web Script Framework allows for a custom status response template to be rendered, but this happens only if the status.redirect value is set to true. A default status response template is provided by the Web Script Framework, which renders everything known about the status, so it is not necessary to develop your own; however, you can create a custom status response template. If the value of status.redirect is set to false, the status code is set on the response, but the response template for the requested format is rendered anyway.
However, the web browser is not the exclusive client from which to invoke a web script. Any client capable of sending HTTP requests and receiving HTTP responses can be used. A good example is the cURL client that has full support for the HTTP protocol and is often used for testing the various capabilities of web scripts.
Although a client can use HTTP directly to invoke web scripts, the Web Script Framework also provides many helpers for invoking web scripts from environments that do not know HTTP. This allows the invocation of a web script using a mechanism that is natural to the calling environment and to the developer who knows the calling environment.
For example, helpers are provided that allow the following clients to naturally invoke web scripts:
A carefully developed web script can be used from multiple environments without the need to change its implementation. For example, a web script for displaying your Alfresco Community Edition checked-out documents can be used standalone directly in a web browser, as a portlet in a JSR-168 portal, or as a dashlet in Alfresco Share.
These helpers include:
For example, to invoke the following web script through an HTTP POST but inform the Web Script Framework to really perform a GET, you would type the following in the command line:
For the equivalent of the override header, but expressed as a query parameter, you would type the following in the command line:
Tunneling HTTP methods is a last resort that should be used only when no other workaround is available. Each HTTP method has its own characteristics such as how it is cached, which HTTP clients and intermediaries expect. When tunneling these methods through HTTP POST, those expectations can no longer be met.
Method overrides are also supported when issuing HTTP GET requests through the alf_method query parameter. This is particularly useful for testing some non-GET methods by using the web browser.
In this situation, web scripts provide a mechanism to force an HTTP response to indicate success in its response header; however, the response body will still represent the content as if a non-success status had occurred, allowing a client to interrogate error codes and messages, if provided by the web script.
To force success, the alf-force-success-response header is set on the HTTP request whose value is always set to true. For example, to force a success response status for a request to retrieve children of a folder that does not exist, you would type the following in the command line:
Although the response status code is 200 (which means Success), the body of the response will still represent a failure and include details such as the real status code (in this case, 404, which means Not Found) and an error message.
Web scripts also provide this mechanism, which wraps the JSON response text in parentheses and a function name of your choosing. A callback is invoked by adding the following URL query parameter to the web script request:
The function parameter specifies the name of a client-side JavaScript function to invoke.
As a web script executes it will perform operations such as creating a new document in the repository. While it seems logical to handle possible exceptions, such as failure to create a document (possibly due to permissions, or the existence of a document with the same name in the same folder), this should be avoided at the web script level. Such exceptions will be handled appropriately by the repository. In practice you should only carry out exception handling for exceptions that you know are not handled at a lower layer of Alfresco Community Edition.
The Web Script Framework does not invent its own caching approach but relies on the caching protocol defined by HTTP. Each web script specifies how it is to be cached, which the Web Script Framework translates into appropriate HTTP headers when it is invoked. A third party HTTP cache that is deployed as part of the application then caches the web script response.
It is often necessary to cache the retrieval of content streams of documents residing in the repository as these can be large in size. A typical setup to support this scenario (as shown in the following figure) is to place an HTTP cache proxy between the client (for example, a web browser) and the Alfresco Community Edition server.
A pre-built, out-of-the-box web script exists for retrieving the content stream of a document residing in the repository. This web script is CMIS compliant and also specifies its HTTP caching requirements. With the HTTP cache proxy deployed, the content responses are cached intelligently and the cache is only updated when the content is updated in the repository. This setup will also cache all other responses from web scripts that indicate how they are to be cached.
When developing a web script, you can specify its caching requirements, such as how long to keep the response in the cache or how to calculate the hash for the response. It is important to note that the Web Script Framework does not actually perform any caching. Instead, Alfresco Community Edition relies on one of the many HTTP caches already available, such as Squid (www.squid-cache.org), an HTTP caching proxy. Therefore, you must either embed an HTTP cache in your client or deploy an HTTP-cache proxy in front of the Alfresco Community Edition server to enable caching.
The cache root object provides the following API:
The optional <cache> element of the web script descriptor provides the following cache flags:
<webscript> <shortname>Design time cache sample</shortname> <url>/cache</url> <authentication>user</authentication> <cache> <never>false</never> <public>false</public> <mustrevalidate/> </cache> </webscript>
To support restricted access, a web script can specify its authentication requirements. There are four levels of required authentication:
An authenticated web script has access to all the services of the Alfresco Community Edition server and thus can perform any operation, although it still adheres to the permissions of the authenticated user.
JSR-168 Authenticator only works if running on the repository tier, and it does not work for web scripts running in the Share tier. Surf has support for JSR-168 portlets built-in.
If you are using the Alfresco checked-out documents web script as a JSR-168 portlet configured into your portal, when you launch the portal the portal itself asks you to log in. The web script needs to know who is authenticated, so the Web Script Framework communicates with the portal to determine the currently authenticated user. When the web script is rendered in the portal page, the web script is invoked as the portal user.
Behind the scenes, the Web Script Framework chooses the most appropriate option for specifying the user identity, either HTTP Basic authentication, ticket, or guest when invoking the web script. The same mechanism is used for Alfresco Share.
Web scripts can handle URL-encoded submissions as other requests, where the web script parses the URI to extract the form data. However, the URL-encoded approach is inefficient for sending large quantities of binary data or text containing non-ASCII characters.
To submit forms containing files, non-ASCII, and binary data, the multipart form data content type must be used; however, this type of request is not as simple to parse for the server. Given the common requirement to submit files to the repository, the Web Script Framework provides explicit support for handling multipart form data submissions by hiding the complexity of request parsing from the developer of the web script.
Related tasks:
For human-readable web script responses it is often necessary to render the output in the preferred language of the user or the preferred language of the client. This means that human-readable text cannot be placed directly in the web script response template.
Therefore, the Web Script Framework uses the common practice of allowing text to be placed into resource bundles, where a resource bundle exists for each supported language.
<webscript> <shortname>I18n Sample</shortname> <description>Internationalization Sample</description> <url>/i18n</url> </webscript>
greeting=Hello farewell=Goodbye
${msg("greeting")}. ${msg("farewell")}
The response is: Hello. Goodbye.
Each resource bundle adheres to the naming convention defined by the Web Script Framework, which are structured as follows: <web script id>.<http method>[_<locale>].properties
The <web script id> identifies the web script and must be the same as the web script ID defined in the file name of the associated Web script description document. The <http method> specifies which HTTP method will initiate the web script and must be the same as the associated web script description document.
The optional <locale> identifies the language for which this resource bundle applies. If not specified, the resource bundle is treated as the fallback set of values if no other relevant resource bundle for the required language can be found.
Finally, all resource bundle file names end with .properties. This indicates to the Web Script Framework that the file is a resource bundle.
greeting=Guten Tag farewell=Auf Wiedersehen
This time you have created a resource bundle for the German language as identified by the locale of de. Locales are specified as follows: <language>[_<country>][_<variant>]
The language argument is a valid ISO language code, which is a lowercase, two-letter code as defined by ISO-639. The optional country argument is a valid ISO country code, which is an uppercase, two-letter code as defined by ISO-3166. Finally, the optional variant argument is a vendor-or web browser–specific code.
The response is: Guten Tag. Auf Wiedersehen.
To quickly provision your site for many different countries and languages, you can provide a message bundle for the Alfresco Share configuration. To do so, you need to wire in your own message bundle to Share that overrides Share’s default message bundle values.
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'> <beans> <bean id="webscripts.resources" class="org.alfresco.i18n.ResourceBundleBootstrapComponent"> <property name="resourceBundles"> <list> <value>alfresco.messages.webscripts</value> <value>alfresco.messages.slingshot</value> <value>alfresco.web-extension.messages.kbsite</value> </list> </property> </bean> </beans>
This Spring bean adds support for an additional message bundle called kbsite.properties located under web-extension/messages. In this message bundle, you might define the following key/value pairs:
page.kbSiteDashboard.title=Knowledge Base Site Dashboard page.kbSiteDashboard.description=Knowledge Base site's dashboard page title.kbSite=Knowledge Base Site
These are the same keys that the preset configuration and web script were looking for. You can now fully internationalize your new site preset. You can provide bundles so that the Create Site wizard works for languages such as Spanish or Mandarin Chinese.
Java-backed web scripts are useful when you want to:
Unlike scripted web scripts, Java-backed web scripts require more tooling for their development as you must compile the Java source code, package, and deploy to the Alfresco Community Edition server.
A Java-backed web script is constructed like a scripted web script, except that a Java class replaces the controller script. It still has the same intent of encapsulating the behavior of the web script and producing a model for subsequent rendering by a response template. Alfresco Community Edition is aware of the Java class through Spring Framework configuration, which identifies the Java class as being the behavior for the web script. All other components are exactly the same as those for scripted web scripts.
This interface defines the following two methods that must be implemented:
/** * Gets the Web script Description * * @return the Web script description */ public WebScriptDescription getDescription(); /** * Execute the Web script * * @param req the Web script request * @param res the Web script response */ public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException;
The first method, getDescription(), returns a WebScriptDescription object, which is a Java representation of the web script description XML document. The second method, execute(), is invoked by the Web Script Framework to initiate the web script.
The Web Script Framework also provides two Java classes that implement the difficult parts of this interface, which you can extend as a starting point. The simplest helper Java class is named as follows: org.alfresco.web.scripts.AbstractWebScript
This helper provides an implementation of getDescription() but does not provide any execution assistance, which it delegates to its derived class. This allows a Java-backed web script to take full control of the execution process, including how output is rendered to the response.
The other helper Java class is named: org.alfresco.web.scripts.DeclarativeWebScript
By default, all web scripts implemented through scripting alone are backed by the DeclarativeWebScript Java class. There is one special hook point that makes this a useful class for your own Java-backed web scripts to extend. Prior to controller script execution, DeclarativeWebScript invokes the template method executeImpl(), which it expects derived Java classes to implement.
protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache)
This is where the behavior of a custom Java-backed web script is encapsulated, including the population of the web script model, which is returned from this method.
The Java Folder Listing web script uses DeclarativeWebScript for its starting point.
... public class JavaDir extends DeclarativeWebScript { ... protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache) { ... return model; } ... }
The model returned from executeImpl() is passed to the response template for subsequent rendering. Prior to template rendering, the model can also be accessed and further refined by a controller script, if one happens to be provided for the web script. Apart from implementing the WebScript interface, there are no other web script demands on the Java class. You can give the Java class any name and place it in any Java package.
Links:
[1] http://docs.alfresco.com/support/concepts/su-product-lifecycle.html
[2] https://docs.alfresco.com/../concepts/dev-platform-arch.html
[3] https://docs.alfresco.com/../concepts/ws-types.html
[4] https://en.wikipedia.org/wiki/Representational_state_transfer
[5] https://docs.alfresco.com/../pra/1/topics/pra-welcome.html
[6] https://docs.alfresco.com/../concepts/surf-connectors-endpoints.html
[7] https://docs.alfresco.com/dev-extension-points-content-model.html
[8] https://en.wikipedia.org/wiki/Word_stem
[9] https://en.wikipedia.org/wiki/Root_%28linguistics%29
[10] https://github.com/Alfresco/alfresco-sdk/blob/master/docs/getting-started.md
[11] https://docs.alfresco.com/../concepts/ws-component-name.html
[12] https://docs.alfresco.com/API-JS-rootscoped.html
[13] https://docs.alfresco.com/API-FreeMarker-defaultmodel.html
[14] https://docs.alfresco.com/../concepts/ws-presentation-locations.html
[15] https://docs.alfresco.com/../concepts/ws-caching-about.html
[16] https://docs.alfresco.com/../concepts/dev-extensions-share-surf-web-scripts.html
[17] https://docs.alfresco.com/dev-extension-points-data-lists.html
[18] https://github.com/Alfresco/alfresco-sdk-samples/tree/alfresco-51/all-in-one/add-web-script-repo
[19] https://github.com/Alfresco/alfresco-sdk-samples/tree/alfresco-51/all-in-one/custom-data-list-repo
[20] http://ecmarchitect.com/alfresco-developer-series-tutorials/webscripts/tutorial/tutorial.html
[21] https://docs.alfresco.com/../tasks/ws-config.html
[22] https://docs.alfresco.com/../tasks/ws-cache-using.html
[23] https://docs.alfresco.com/../tasks/ws-request-process.html
[24] https://docs.alfresco.com/../concepts/ws-overview.html
[25] https://docs.alfresco.com/../concepts/dev-platform-extension-points.html
[26] https://docs.alfresco.com/../concepts/ws-intro.html
[27] https://docs.alfresco.com/../concepts/ws-framework.html
[28] https://docs.alfresco.com/../concepts/ws-invoke-where.html
[29] https://docs.alfresco.com/../concepts/ws-client-limitations.html
[30] https://docs.alfresco.com/../concepts/ws-exception-handling.html
[31] https://docs.alfresco.com/../concepts/ws-authenticating.html
[32] https://docs.alfresco.com/../concepts/ws-forms-about.html
[33] https://docs.alfresco.com/../concepts/ws-I18N.html
[34] https://docs.alfresco.com/../concepts/ws-java-backed-webscripts.html
[35] https://docs.alfresco.com/../references/dev-extension-points-webscripts.html
[36] https://docs.alfresco.com/../concepts/ws-types-data.html
[37] https://docs.alfresco.com/../concepts/ws-types-presentation.html
[38] https://docs.alfresco.com/../concepts/ws-components.html
[39] https://docs.alfresco.com/../concepts/ws-component-place.html
[40] https://docs.alfresco.com/../concepts/ws-anatomy.html
[41] https://docs.alfresco.com/../concepts/ws-uri-template.html
[42] https://docs.alfresco.com/../concepts/ws-format-reader.html
[43] https://docs.alfresco.com/../concepts/ws-resp-code-template.html
[44] https://docs.alfresco.com/../concepts/ws-desc-doc.html
[45] https://docs.alfresco.com/../concepts/ws-controll-script.html
[46] https://docs.alfresco.com/../concepts/ws-resp-template.html
[47] https://docs.alfresco.com/ws-desc-doc.html
[48] https://docs.alfresco.com/ws-controll-script.html
[49] https://docs.alfresco.com/ws-resp-template.html
[50] https://docs.alfresco.com/../references/api-wsdl-webscript-descriptor-language-reference.html
[51] https://docs.alfresco.com/../concepts/ws-resp-code-set.html
[52] https://docs.alfresco.com/../concepts/ws-tunneling-http-methods.html
[53] https://docs.alfresco.com/../concepts/ws-forcing-success.html
[54] https://docs.alfresco.com/../concepts/ws-json-callbacks.html
[55] http://curl.haxx.se/docs/manual.html
[56] http://developer.yahoo.com/javascript/json.html#callbackparam
[57] https://docs.alfresco.com/../concepts/ws-runtime-cache-controls.html
[58] https://docs.alfresco.com/../concepts/ws-desc-cache-controls.html
[59] https://docs.alfresco.com/ws-runtime-cache-controls.html
[60] https://docs.alfresco.com/ws-desc-cache-controls.html
[61] https://docs.alfresco.com/../concepts/ws-custom-client-authentication.html
[62] https://docs.alfresco.com/../tasks/ws-hello-world-create.html
[63] https://docs.alfresco.com/../tasks/ws-specify-user-identity.html
[64] https://docs.alfresco.com/../references/api-wsdl-authentication.html
[65] https://docs.alfresco.com/../tasks/ws-forms-process.html
[66] https://docs.alfresco.com/../tasks/ws-I18N.html
[67] https://docs.alfresco.com/../tasks/ws-I18N-german.html
[68] https://docs.alfresco.com/../concepts/kb-preset-internationalization.html
[69] https://docs.alfresco.com/../concepts/ws-and-Java.html