As I have mentioned before, I don't plan on my (hypothetical) bloggers and editors using the Alfresco web UI to manage the content in the CMS. I want to provide them with an interface similar to Drupal's rather than the file based interface Alfresco provides by default. To modify Alfresco's web UI to support this would require quite a lot of work, including having to learn Java Server Faces, upon which Alfresco's web UI is based, and which I know nothing about, and given that I don't use it at the moment for anything else, don't feel particularly compelled to learn.
Instead, I plan on building a Java based webapp from the ground up, and have it proxy through to Alfresco over HTTP via REST scripts using Alfreso's Webscript framework. This approach appears to be quite popular, Alfresco Developer's Guide has an entire chapter devoted to this, and there is also a Alfresco's Web Scripts page also has quite a bit of documentation about this.
Essentially, on the Alfresco side, one writes a "controller", consisting of an XML descriptor, a Webscript and one or more views to expose a given function. Each view has a URL associated with it. Webscripts can be written using Java or Javascript. Views are written using Freemarker Template Language (FTL). There is a strict naming convention that needs to be followed for everything to work, since the controller is run dynamically using the Webscript framework.
I made a conscious decision to keep things in Java as far as possible, so even though I liked the Javascript API (it is as comprehensive as the Java API, results in more concise code and you can write and debug code in the running server, which is a great timesaver), I decided to check out the Java way first. I do want to use the Javascript API (at least for Webscripts) at some point in the future though.
So anyway, I built two Webscripts this week, which I describe below.
Search Webscript
The Search Webscript does a fulltext search for a given term. It is intended to be used from a in-page search widget, like the ones you find on the top right corner of some websites. The user enters a term or phrase and gets back a page of search results.
Obviously, the Search Webscript uses the Alfresco Search Service to retrieve the results using a Lucene query. The difference is that the results are filtered by user role, ie, a blogger only gets to search in documents that are either in his inbox, or have been submitted by him for review, or have been published already. An editor on the other hand, gets to search in all documents in review, and all published documents, but not documents that are in Draft state in blogger's inboxes.
Descriptor
Here is the XML descriptor for the Search WebScript. It needs to be stored in the directory shown in the Source comment, along with the FTL files. It defines the URLs that are exposed by the controller, along with some transaction and authentication requirements.
1 2 3 4 5 6 7 8 9 10 11 12 | <?xml version="1.0" encoding="UTF-8"?>
<!-- Source: config/alfresco/extension/templates/webscripts/com/mycompany/search/search.get.desc.xml -->
<webscript>
<shortname>Document Search [mycompany]</shortname>
<description>Document Search [mycompany]</description>
<url>/mycompany/search</url>
<url>/mycompany/search.json</url>
<url>/mycompany/search.html</url>
<format default="json">extension</format>
<authentication>none</authentication>
<transaction>none</transaction>
</webscript>
|
Java Code
A design strategy I have followed consistently in all my Alfresco work so far is to delegate the work involving the Alfresco Foundation API to a helper class. This is to make it easier to test functionality via unit tests on the command line (although it does take a while for the unit tests to run, since it has to spin up the Alfresco application context each time). Consequently, the Java code that follows isn't particularly interesting, but serves to show the general structure of a Webscript Java controller. I show the WebscriptHelper class later in the post.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // Source: src/java/com/mycompany/alfresco/extension/webscripts/GetSearch.java
package com.mycompany.alfresco.extension.webscripts;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.web.scripts.Cache;
import org.alfresco.web.scripts.DeclarativeWebScript;
import org.alfresco.web.scripts.Status;
import org.alfresco.web.scripts.WebScriptException;
import org.alfresco.web.scripts.WebScriptRequest;
import org.apache.commons.lang.StringUtils;
public class GetSearch extends DeclarativeWebScript {
private WebscriptHelper helper;
public void setHelper(WebscriptHelper helper) {
this.helper = helper;
}
@Override
public Map<String,Object> executeImpl(WebScriptRequest request,
Status status, Cache cache) {
String query = request.getParameter("query");
// TODO: replace with alf_ticket?
String user = request.getParameter("user");
if (StringUtils.isEmpty(user) || StringUtils.isEmpty(query)) {
throw new WebScriptException(
"Can't find mandatory parameters query term and/or user");
}
Map<String,Object> model = new HashMap<String,Object>();
helper.search(model, query, user);
model.put("query", query);
model.put("user", user);
return model;
}
}
|
As you can see, my Webscript controller extends DeclarativeWebScript and overrides its executeImpl(WebRequest, Status, Cache)::Map
HTML Template
The HTML template is quite simple, we don't really need this for my setup to work (I plan on using the JSON data on the client side), but it helps for debugging the service. Besides I wanted to check out FTL (I've used Velocity but not Freemarker before this). Here is the HTML template.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <#-- Source: config/alfresco/extension/templates/webscripts/com/mycompany/search/search.get.html.ftl -->
<html>
<head>
<title>Search results for "${query}"</title>
</head>
<body>
<h2>Search Results for "${query}"</h2>
<#list posts as post>
<p>
<#if post.pubState == "Draft" || post.pubState == "Review">
<b><a href="/preview/${post.id}">${post.title}</a></b>
</#if>
<#if post.pubState == "Published">
<b><a href="${post.furl}">${post.title}</a></b>
</#if>
<br/>${post.description?html}
<br/><br/><font size="-1">
<b>Status: </b>${post.pubState}
<b>Created By: </b>${post.creator}
<b>Dated: </b>${post.created?datetime}
</font>
</p>
</#list>
</body>
</html>
|
JSON Template
The corresponding JSON template looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <#-- Source: config/alfresco/extension/templates/webscripts/com/mycompany/search/search.get.json.ftl -->
{"model":
{"query" : "${query}"},
{"user" : "${user}"},
{"posts" : [
<#list posts as post>
{"id" : "${post.id}",
"title" : "${post.title}",
<#if post.pubState == "Draft" || post.pubState == "Review">
"furl" : "/preview/${post.id}",
<#else>
"furl" : "${post.furl}",
</#if>
"description" : ${post.description?html}",
"pubState" : "${post.pubState}",
"creator" : "${post.creator}",
"created" : "${post.created?datetime}"
},
</#list>
]}
}
|
Example HTML Output
An example output for a search with user=happy and query=drupal is shown below. The JSON output is less spectacular (relatively speaking), but contains the same data. This is in response to the URL:
1 | http://localhost:8080/alfresco/service/mycompany/search.html?query=drupal&user=happy
|
Dashboard Webscript
The Dashboard Webscript returns the documents that a user needs to work on at the current time. Like the Search Webscript, the contents of the dashboard is dependent on the user. For example, a blogger only sees the contents of his home directory, while an editor will see separate buckets containing lists of documents for Review, Published documents, etc.
Descriptor
The descriptor for this webscript is similar to the one for search. Here it is.
1 2 3 4 5 6 7 8 9 10 11 12 | <?xml version="1.0" encoding="UTF-8"?>
<!-- Source: config/alfresco/extension/templates/webscripts/com/mycompany/dashboard/dashboard.get.desc.xml -->
<webscript>
<shortname>Dashboard [mycompany]</shortname>
<description>Dashboard [mycompany]</description>
<url>/mycompany/dashboard</url>
<url>/mycompany/dashboard.json</url>
<url>/mycompany/dashboard.html</url>
<format default="json">extension</format>
<authentication>none</authentication>
<transaction>none</transaction>
</webscript>
|
Java Code
Again, not much to say about this, its similar to the one for Search.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // Source: src/java/com/mycompany/alfresco/extension/webscripts/GetDashboard.java
package com.mycompany.alfresco.extension.webscripts;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.web.scripts.Cache;
import org.alfresco.web.scripts.DeclarativeWebScript;
import org.alfresco.web.scripts.Status;
import org.alfresco.web.scripts.WebScriptException;
import org.alfresco.web.scripts.WebScriptRequest;
import org.apache.cxf.common.util.StringUtils;
public class GetDashboard extends DeclarativeWebScript {
private WebscriptHelper helper;
public void setHelper(WebscriptHelper helper) {
this.helper = helper;
}
@Override
public Map<String,Object> executeImpl(WebScriptRequest request,
Status status, Cache cache) {
String user = request.getParameter("user");
if (StringUtils.isEmpty(user)) {
throw new WebScriptException("Can't find mandatory parameter user");
}
Map<String,Object> model = new HashMap<String,Object>();
helper.getDashboard(model, user);
model.put("user", user);
return model;
}
}
|
HTML Template
The HTML Template for the Dashboard is shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | <#-- Source: config/alfresco/extension/templates/webscripts/com/mycompany/dashboard/dashboard.get.html.ftl -->
<html>
<head>
<title>Dashboard for ${role?cap_first} ${user}</title>
</head>
<body>
<h2>Welcome ${role?cap_first} ${user}</h2>
<#if inbox??>
<h2>Inbox</h2>
<table cellspacing="3" cellpadding="3" border="0">
<tr>
<td><b>Post Title</b></td>
<td><b>Status</b></td>
<td><b>Created By</b></td>
<td><b>Date</b></td>
</tr>
<#list inbox as post>
<tr>
<td><a href="/preview/${post.id}">${post.title}</a></td>
<td>${post.pubState}</td>
<td>${post.creator}</td>
<td>${post.created?datetime}</td>
</tr>
</#list>
</table>
</#if>
<#if review??>
<h2>Posts Pending Review</h2>
<table cellspacing="3" cellpadding="3" border="0">
<tr>
<td><b>Post Title</b></td>
<td><b>Status</b></td>
<td><b>Created By</b></td>
<td><b>Date</b></td>
</tr>
<#list review as post>
<tr>
<td><a href="/preview/${post.id}">${post.title}</a></td>
<td>${post.pubState}</td>
<td>${post.creator}</td>
<td>${post.created?datetime}</td>
</tr>
</#list>
</table>
</#if>
<#if published??>
<h2>Published Posts</h2>
<table cellspacing="3" cellpadding="3" border="0">
<tr>
<td><b>Post Title</b></td>
<td><b>Status</b></td>
<td><b>Created By</b></td>
<td><b>Date</b></td>
</tr>
<#list published as post>
<tr>
<td><a href="${post.furl}">${post.title}</a></td>
<td>${post.pubState}</td>
<td>${post.creator}</td>
<td>${post.created?datetime}</td>
</tr>
</#list>
</table>
</#if>
<#if scheduled??>
<h2>Posts Scheduled for Publish</h2>
<table cellspacing="3" cellpadding="3" border="0">
<tr>
<td><b>Post Title</b></td>
<td><b>Status</b></td>
<td><b>Created By</b></td>
<td><b>Date</b></td>
</tr>
<#list scheduled as post>
<tr>
<td><a href="/preview/${post.id}">${post.title}</a></td>
<td>${post.pubState}</td>
<td>${post.creator}</td>
<td>${post.created?datetime}</td>
</tr>
</#list>
</table>
</#if>
<#if archived??>
<h2>Archived Posts</h2>
<table cellspacing="3" cellpadding="3" border="0">
<tr>
<td><b>Post Title</b></td>
<td><b>Status</b></td>
<td><b>Created By</b></td>
<td><b>Date</b></td>
</tr>
<#list archived as post>
<tr>
<td><a href="/preview/${post.id}">${post.title}</a></td>
<td>${post.pubState}</td>
<td>${post.creator}</td>
<td>${post.created?datetime}</td>
</tr>
</#list>
</table>
</#if>
</body>
</html>
|
JSON Template
And the JSON Template for the Dashboard.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | <#-- Source: config/alfresco/extension/templates/webscripts/com/mycompany/dashboard/dashboard.get.json.ftl -->
{"model":
{"user" : "${user}"},
{"role" : "${role?cap_first}"},
<#if inbox??>
{"inbox" : [
<#list posts as post>
{"id" : "${post.id}",
"title" : "${post.title}",
"furl" : "/preview/${post.id}",
"pubState" : "${post.pubState}",
"creator" : "${post.creator}",
"created" : "${post.created?datetime}"
},
</#list>
]},
</#if>
<#if review??>
{"review" : [
<#list review as post>
{"id" : "${post.id}",
"title" : "${post.title}",
"furl" : "/preview/${post.id}",
"pubState" : "${post.pubState}",
"creator" : "${post.creator}",
"created" : "${post.created?datetime}"
},
</#list>
]},
</#if>
<#if published??>
{"published" : [
<#list published as post>
{"id" : "${post.id}",
"title" : "${post.title}",
"furl" : "${post.furl}",
"pubState" : "${post.pubState}",
"creator" : "${post.creator}",
"created" : "${post.created?datetime}"
},
</#list>
]},
</#if>
<#if scheduled??>
{"scheduled" : [
<#list scheduled as post>
{"id" : "${post.id}",
"title" : "${post.title}",
"furl" : "/preview/${post.id}",
"pubState" : "${post.pubState}",
"creator" : "${post.creator}",
"created" : "${post.created?datetime}"
},
</#list>
]},
</#if>
<#if archived??>
{"archived" : [
<#list archived as post>
{"id" : "${post.id}",
"title" : "${post.title}",
"furl" : "/preview/${post.id}",
"pubState" : "${post.pubState}",
"creator" : "${post.creator}",
"created" : "${post.created?datetime}"
},
</#list>
]},
</#if>
}
|
Example HTML Output
The XML output representing the dashboard for user "happy" is shown below. This is in response to a URL of the form:
1 | http://localhost:8080/alfresco/service/mycompany/dashboard.html?user=doc
|
Webscript Helper
The Webscript helper does most of the work, as I mentioned above. Not much explanation is required if you are familiar with the Alfresco Java API. It has two methods search() and getDashboard() which are called from the corresponding Webscripts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | // Source: src/java/com/mycompany/alfresco/extension/webscripts/WebscriptHelper.java
package com.mycompany.alfresco.extension.webscripts;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.transaction.UserTransaction;
import org.alfresco.model.ContentModel;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.web.scripts.WebScriptException;
import org.apache.commons.lang.WordUtils;
import org.apache.log4j.Logger;
import org.springframework.util.CollectionUtils;
import com.mycompany.alfresco.extension.model.MyContentModel;
import com.mycompany.alfresco.extension.model.Post;
public class WebscriptHelper {
private final Logger logger = Logger.getLogger(getClass());
private AuthorityService authorityService;
private TransactionService transactionService;
private SearchService searchService;
private NodeService nodeService;
public void setAuthorityService(AuthorityService authorityService) {
this.authorityService = authorityService;
}
public void setTransactionService(TransactionService transactionService) {
this.transactionService = transactionService;
}
public void setSearchService(SearchService searchService) {
this.searchService = searchService;
}
public void setNodeService(NodeService nodeService) {
this.nodeService = nodeService;
}
public void search(Map<String,Object> model,
String term, String userName) throws WebScriptException {
List<Post> posts = new ArrayList<Post>();
ResultSet resultSet = null;
UserTransaction tx = transactionService.getUserTransaction();
try {
tx.begin();
try {
// tailor the query depending on who is calling it
StringBuilder queryBuilder = new StringBuilder();
String role = getRole(userName);
if ("blogger".equals(role)) {
// Blogger can see own posts in Draft and Review, and all
// published posts
queryBuilder.
// all docs with term in user's home directory
append("(+PATH:\"/app:company_home/app:user_homes/sys:").
append(WordUtils.capitalize(userName)).
append("/*\" +").
append(term).
append(") ").
// OR all docs in review containing term and created by user
append("(+PATH:\"/app:company_home/cm:Public/cm:Review/*").
append("\" +cm\\:creator:\"").
append(userName).
append("\" +").
append(term).
append(") ").
// OR all docs in published containing term
append("(+PATH:\"/app:company_home/cm:Public/cm:Published/cm:Live/*").
append("\" +").
append(term).
append(")");
} else if ("editor".equals(role)) {
// Editor can see all posts in Review and Published
queryBuilder.
// all docs with term in Review
append("(+PATH:\"/app:company_home/cm:Public/cm:Review/*").
append("\" +").
append(term).
append(") ").
// all docs with term in Published
append("(+PATH:\"/app:company_home/cm:Public/cm:Published/cm:Live/*").
append("\" +").
append(term).
append(")");
} else {
throw new WebScriptException("Invalid user: " + userName +
". Must have permissions of GROUP_BLOGGER or GROUP_EDITOR");
}
logger.debug("query=" + queryBuilder.toString());
resultSet = searchService.query(
StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,
SearchService.LANGUAGE_LUCENE, queryBuilder.toString());
for (ChildAssociationRef caref : resultSet.getChildAssocRefs()) {
NodeRef nodeRef = caref.getChildRef();
posts.add(getPost(nodeRef));
}
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
if (resultSet != null) { resultSet.close(); }
}
model.put("posts", posts);
} catch (Exception e) {
throw new WebScriptException(e.getMessage(), e);
}
}
public void getDashboard(Map<String,Object> model, String userName)
throws WebScriptException {
UserTransaction tx = transactionService.getUserTransaction();
try {
tx.begin();
try {
String role = getRole(userName);
if ("blogger".equals(role)) {
model.put("role", role);
// contains posts in the blogger's home directory
List<Post> inboxPosts = getPosts(
"/app:company_home/app:user_homes/sys:" +
WordUtils.capitalize(userName));
if (! CollectionUtils.isEmpty(inboxPosts)) {
model.put("inbox", inboxPosts);
}
} else if ("editor".equals(role)) {
model.put("role", role);
// contains posts under review, and...
List<Post> reviewPosts = getPosts(
"/app:company_home/cm:Public/cm:Review");
if (! CollectionUtils.isEmpty(reviewPosts)) {
model.put("review", reviewPosts);
}
// published posts, and...
List<Post> publishedPosts = getPosts(
"/app:company_home/cm:Public/cm:Published/cm:Live");
if (! CollectionUtils.isEmpty(publishedPosts)) {
model.put("published", publishedPosts);
}
// scheduled posts, and...
List<Post> scheduledPosts = getPosts(
"/app:company_home/cm:Public/cm:Published/cm:Scheduled");
if (! CollectionUtils.isEmpty(scheduledPosts)) {
model.put("scheduled", scheduledPosts);
}
// archived posts
List<Post> archivedPosts = getPosts(
"/app:company_home/cm:Public/cm:Published/cm:Archived");
if (! CollectionUtils.isEmpty(archivedPosts)) {
model.put("archived", archivedPosts);
}
}
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
}
} catch (Exception e) {
throw new WebScriptException(e.getMessage(), e);
}
}
private List<Post> getPosts(String path) throws Exception {
List<Post> posts = new ArrayList<Post>();
ResultSet resultSet = null;
try {
String query = "+PATH:\"" + path + "/*\" +ASPECT:\"my:publishable\"";
resultSet = searchService.query(
StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,
SearchService.LANGUAGE_LUCENE, query);
for (ChildAssociationRef caref : resultSet.getChildAssocRefs()) {
NodeRef nodeRef = caref.getChildRef();
posts.add(getPost(nodeRef));
}
} catch (Exception e) {
throw e;
} finally {
if (resultSet != null) { resultSet.close(); }
}
return posts;
}
public String getRole(String userName) {
Set<String> authorities = authorityService.getAuthoritiesForUser(userName);
if (authorities.contains("GROUP_GROUP_BLOGGER")) {
return "blogger";
} else if (authorities.contains("GROUP_GROUP_EDITOR")) {
return "editor";
} else {
return null;
}
}
private Post getPost(NodeRef nodeRef) {
Map<QName,Serializable> props = nodeService.getProperties(nodeRef);
Post post = new Post();
post.setId((String) props.get(ContentModel.PROP_NODE_UUID));
post.setTitle((String) props.get(ContentModel.PROP_TITLE));
post.setFurl((String) props.get(MyContentModel.PROP_FURL));
post.setDescription((String) props.get(ContentModel.PROP_DESCRIPTION));
post.setPubState(props.get(MyContentModel.PROP_PUBSTATE));
post.setCreated((Date) props.get(ContentModel.PROP_CREATED));
post.setCreator((String) props.get(ContentModel.PROP_CREATOR));
return post;
}
}
|
Spring Configuration
Finally, we configure these two webscripts in Spring. We create a mycompamy-scripts-context.xml file in the config/alfresco/extension directory, which is loaded via Alfresco's extension mechanism. It defines the two Webscript beans and the helper.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?xml version="1.0" encoding="UTF-8"?>
<!-- Source: config/alfresco/extension/mycompany-scripts-context.xml -->
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN'
'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>
<bean id="webscript.com.mycompany.search.search.get"
class="com.mycompany.alfresco.extension.webscripts.GetSearch"
parent="webscript">
<property name="helper" ref="webscriptHelper"/>
</bean>
<bean id="webscript.com.mycompany.dashboard.dashboard.get"
class="com.mycompany.alfresco.extension.webscripts.GetDashboard"
parent="webscript">
<property name="helper" ref="webscriptHelper"/>
</bean>
<bean id="webscriptHelper"
class="com.mycompany.alfresco.extension.webscripts.WebscriptHelper">
<property name="authorityService" ref="authorityService"/>
<property name="transactionService" ref="transactionService"/>
<property name="searchService" ref="searchService"/>
<property name="nodeService" ref="nodeService"/>
</bean>
</beans>
|
What's Next?
I am nearing the end of my Alfresco customization project. I still have some more Webscripts to write, but they are likely to be repetitive, so I don't plan on writing about them unless there is something to write about. I do have the CMS client webapp to build, but I plan to take a short break in order to learn Spring-Roo, with which I plan to build the client.
No comments:
Post a Comment
Comments are moderated to prevent spam.