The past couple of years I’ve seen many examples of using Liferay’s built-in JSON services in various ways. The architecture and syntax of this feature has undergone several refinements in the past few versions, so the documentation/examples you can find via a website search are usually slightly wrong and misleading. Recently I saw this thread come alive and decided to sit down and make a non-trival read/write example of using Liferay’s JSON services work.
Bear in mind that this applies to Liferay 6.0.x (I am using 6.0.6 CE in the examples). In Liferay 6.1 there are new interfaces coming like (such asRESTful interfaces), ability to do automatic serialization and improved method argument passing, and there are also existing “heavy lifting” web service interfaces like SOAP endpoints that one can use. So this is not the only way to do things. But it is great for prototyping and getting things to work quickly without dragging a bunch of dependencies and debugging hard-to-understand wire protocols. I hope this example is still relevant!
The only dependency I am using here is Apache Commons HTTPClient. I also decided to write it in Java (as opposed to Ray’s earlier example on 5.2 in PHP).
Couple of things to be aware of:
- By default, access is through Liferay’s tunnel-web web app. So the proper full URL in a default Liferay 6.0.6 install is
http://localhost:8080/tunnel-web/secure/json
.
- Since it is a “secure” (authenticated) interface we need to provide a username and password. This is done using HTTP Basic Authentication, which of course is not appropriate for a production environment, since the password is unencrypted (it is instead base64-encoded following HTTP Basic Authentication). The default username/password is “test/test”.
- There’s no error checking whatsoever here. You should add it for a real world scenario.
First Example
So, here’s the first example. A simple “Hello World” that does the same thing as Ray’s example, only using Liferay 6 and written in simple Java. It simply access the “Country” service and returns a list of country entities known to Liferay.
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
public class TestLiferayJSON {
public static void main(String[] args) throws Exception {
HttpHost targetHost = new HttpHost("localhost", 8080, "http");
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getCredentialsProvider().setCredentials(
new AuthScope(targetHost.getHostName(), targetHost.getPort()),
new UsernamePasswordCredentials("test", "test"));
// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate BASIC scheme object and add it to the local
// auth cache
BasicScheme basicAuth = new BasicScheme();
authCache.put(targetHost, basicAuth);
// Add AuthCache to the execution context
BasicHttpContext ctx = new BasicHttpContext();
ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);
HttpPost post = new HttpPost("/tunnel-web/secure/json");
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portal.service.CountryServiceUtil"));
params.add(new BasicNameValuePair("serviceMethodName", "getCountries"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity);
HttpResponse resp = httpclient.execute(targetHost, post, ctx);
resp.getEntity().writeTo(System.out);
System.out.println();
httpclient.getConnectionManager().shutdown();
}
}
If you compile and run this (you’ll have to download the Apache Commons HTTPClient libraries and put them on the classpath and/or add them as dependencies in your IDE), then you should see something like this on your output screen (this is the returned content from the HTTP POST):
[{"countryId":20,"idd":"093","name":"Afghanistan","active":true,"a2":"AF","number":"4","a3":"AFG"},{"countryId":21,"idd":"355","name":"Albania","active":true,"a2":"AL","number":"8","a3":"ALB"},
.....many more countries listed
{"countryId":227,"idd":"263","name":"Zimbabwe","active":true,"a2":"ZW","number":"716","a3":"ZWE"}]
Notice where I specify the username/password using Apache HTTPClient APIs. This should be easily translatable to your favorite client (or if you are using curl or some other RESTful client test console such as rest-client and you want to specify the authentication header manually, use an Authorization header with a value of Basic dGVzdDp0ZXN0Cg==
)
Also note the serviceClassName parameter. This name (com.lifery.portal.service.CountryServiceUtil
) specifies the service name (and maps to an actual class). Not all services have remote service endpoints (for example, there’s nocom.liferay.portlet.social.service.SocialActivityServiceUtil
for creating new activity stream items. I wish there were).
The parameters are encoded into the body of the HTTP POST request using the Apache Commons HTTPClient’sUrlEncodedFormEntity
utility. This is the same as Ray’s $a->addPostData
examples in PHP.
Second Example
Ok, now that the easy one works (it does work for you, right?), let’s move on to something trickier using the Web Content system. Notice that this system used to be called “Journal” so all the APIs refer to the Journal Service since the actual APIs were not changed in the interest of compatibility. This second example calls the JournalArticle service to retrieve a sample article using the default install’s groupId of 10156 and the articleId of 10455. These numbers are automatically generated during initial startup the first time and may be different for you. If they are you’ll need to change them. You can find them through the Control Panel by going to Communities -> Actions -> Edit to find the groupId, and pick any articleId from Web Content.
This example calls a specific method by identifying it using its formal parameters and passes the values for the parameters.
public static void journal() throws Exception {
HttpHost targetHost = new HttpHost("localhost", 8080, "http");
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getCredentialsProvider().setCredentials(
new AuthScope(targetHost.getHostName(), targetHost.getPort()),
new UsernamePasswordCredentials("test", "test"));
// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate BASIC scheme object and add it to the local
// auth cache
BasicScheme basicAuth = new BasicScheme();
authCache.put(targetHost, basicAuth);
// Add AuthCache to the execution context
BasicHttpContext ctx = new BasicHttpContext();
ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);
HttpPost post = new HttpPost("/tunnel-web/secure/json");
// create Liferay API parameters
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portlet.journal.service.JournalArticleServiceUtil"));
params.add(new BasicNameValuePair("serviceMethodName", "getArticle"));
params.add(new BasicNameValuePair("serviceParameters", "[groupId,articleId]"));
params.add(new BasicNameValuePair("groupId", "10156"));
params.add(new BasicNameValuePair("articleId", "10455"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity);
// make actual HTTP request and print results to System.out
HttpResponse resp = httpclient.execute(targetHost, post, ctx);
resp.getEntity().writeTo(System.out);
httpclient.getConnectionManager().shutdown();
}
Notice here that the “setup” is exactly the same as before, only the parameters are different. Also note that the list of parameter names (serviceParameters
) starts and ends with brackets. It’s an array of Strings! So don’t forget the brackets.
The getArticle method returns a JSON-encoded article from Liferay’s web content system, so if this example works for you, you should get this on your output stream:
{"urlTitle":"welcome","indexable":true,"statusDate":"1287600093000","type":"general","smallImageId":10458,"articleId":"10455","version":1,"id":10456,"title":"Welcome","description":"","userId":10134,"userName":" ","smallImage":false,"createDate":"1287600093000","displayDate":"1201824000000","smallImageURL":"","expirationDate":"","status":0,"statusByUserName":" ","reviewDate":"","modifiedDate":"1287600093000","content":...
Third Example
Ok, now that you a trivial and almost-trivial example, let’s do something more interesting. Let’s add (and remove) a Journal Article (this is what the initial thread asked about anyway).
Here are two methods: addArticle
and removeArticle
. They use a hard-coded 60000 for articleId
to make removal easy. There are a ton of parameters for addArticle (29 to be exact), and since there are multiple addArticle
methods in theJournalArticleServiceUtil
class we have to specify a serviceParameterTypes
list to tell Liferay which service API we wish to invoke. So the parameter list gets nasty and I didn’t do a very good job of coding it to look nice. You can do that though.
public static void addArticle() throws Exception {
HttpHost targetHost = new HttpHost("localhost", 8080, "http");
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getCredentialsProvider().setCredentials(
new AuthScope(targetHost.getHostName(), targetHost.getPort()),
new UsernamePasswordCredentials("test", "test"));
// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate BASIC scheme object and add it to the local
// auth cache
BasicScheme basicAuth = new BasicScheme();
authCache.put(targetHost, basicAuth);
// Add AuthCache to the execution context
BasicHttpContext ctx = new BasicHttpContext();
ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);
HttpPost post = new HttpPost("/tunnel-web/secure/json");
Calendar yesterday = Calendar.getInstance();
yesterday.add(Calendar.DAY_OF_YEAR, -1);
Calendar nextWeek = Calendar.getInstance();
nextWeek.add(Calendar.WEEK_OF_YEAR, 1);
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portlet.journal.service.JournalArticleServiceUtil"));
params.add(new BasicNameValuePair("serviceMethodName", "addArticle"));
params.add(new BasicNameValuePair("serviceParameters", "[groupId,articleId,autoArticleId,title,description,content,type,structureId,templateId,displayDateMonth,displayDateDay,displayDateYear,displayDateHour,displayDateMinute,expirationDateMonth,expirationDateDay,expirationDateYear,expirationDateHour,expirationDateMinute,neverExpire,reviewDateMonth,reviewDateDay,reviewDateYear,reviewDateHour,reviewDateMinute,neverReview,indexable,articleURL,serviceContext]"));
params.add(new BasicNameValuePair("serviceParameterTypes", "[long,java.lang.String,boolean,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,int,int,int,int,int,int,int,int,int,int,boolean,int,int,int,int,int,boolean,boolean,java.lang.String,com.liferay.portal.service.ServiceContext]"));
params.add(new BasicNameValuePair("groupId", "10156"));
params.add(new BasicNameValuePair("articleId", "60000"));
params.add(new BasicNameValuePair("autoArticleId", "false"));
params.add(new BasicNameValuePair("title", "Test JSON Article"));
params.add(new BasicNameValuePair("description", "Test JSON Description"));
params.add(new BasicNameValuePair("content", "<?xml version='1.0' encoding='UTF-8'?><root available-locales=\"en_US\" default-locale=\"en_US\"><static-content language-id=\"en_US\"><![CDATA[<p>\n" +
"\ttest content</p>]]></static-content></root>"));
params.add(new BasicNameValuePair("type", "general"));
params.add(new BasicNameValuePair("structureId", ""));
params.add(new BasicNameValuePair("templateId", ""));
params.add(new BasicNameValuePair("displayDateMonth", "" + (1 + yesterday.get(Calendar.MONTH))));
params.add(new BasicNameValuePair("displayDateDay", "" + yesterday.get(Calendar.DAY_OF_MONTH)));
params.add(new BasicNameValuePair("displayDateYear", "" + yesterday.get(Calendar.YEAR)));
params.add(new BasicNameValuePair("displayDateHour", "" + yesterday.get(Calendar.HOUR_OF_DAY)));
params.add(new BasicNameValuePair("displayDateMinute", "" + yesterday.get(Calendar.MINUTE)));
params.add(new BasicNameValuePair("expirationDateMonth", "" + (1 + nextWeek.get(Calendar.MONTH))));
params.add(new BasicNameValuePair("expirationDateDay", "" + nextWeek.get(Calendar.DAY_OF_MONTH)));
params.add(new BasicNameValuePair("expirationDateYear", "" + nextWeek.get(Calendar.YEAR)));
params.add(new BasicNameValuePair("expirationDateHour", "" + nextWeek.get(Calendar.HOUR_OF_DAY)));
params.add(new BasicNameValuePair("expirationDateMinute", "" + nextWeek.get(Calendar.MINUTE)));
params.add(new BasicNameValuePair("neverExpire", "false"));
params.add(new BasicNameValuePair("reviewDateMonth", "" + (1 + nextWeek.get(Calendar.MONTH))));
params.add(new BasicNameValuePair("reviewDateDay", "" + nextWeek.get(Calendar.DAY_OF_MONTH)));
params.add(new BasicNameValuePair("reviewDateYear", "" + nextWeek.get(Calendar.YEAR)));
params.add(new BasicNameValuePair("reviewDateHour", "" + nextWeek.get(Calendar.HOUR_OF_DAY)));
params.add(new BasicNameValuePair("reviewDateMinute", "" + nextWeek.get(Calendar.MINUTE)));
params.add(new BasicNameValuePair("neverReview", "false"));
params.add(new BasicNameValuePair("indexable", "true"));
params.add(new BasicNameValuePair("articleURL", "articleURL"));
params.add(new BasicNameValuePair("serviceContext", "{}"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity);
HttpResponse resp = httpclient.execute(targetHost, post, ctx);
System.out.println(resp.getStatusLine());
resp.getEntity().writeTo(System.out);
httpclient.getConnectionManager().shutdown();
}
public static void removeArticle() throws Exception {
HttpHost targetHost = new HttpHost("localhost", 8080, "http");
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.getCredentialsProvider().setCredentials(
new AuthScope(targetHost.getHostName(), targetHost.getPort()),
new UsernamePasswordCredentials("test", "test"));
// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate BASIC scheme object and add it to the local
// auth cache
BasicScheme basicAuth = new BasicScheme();
authCache.put(targetHost, basicAuth);
// Add AuthCache to the execution context
BasicHttpContext ctx = new BasicHttpContext();
ctx.setAttribute(ClientContext.AUTH_CACHE, authCache);
HttpPost post = new HttpPost("/tunnel-web/secure/json");
Calendar now = Calendar.getInstance();
Calendar nextWeek = Calendar.getInstance();
nextWeek.add(Calendar.WEEK_OF_YEAR, 1);
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("serviceClassName", "com.liferay.portlet.journal.service.JournalArticleServiceUtil"));
params.add(new BasicNameValuePair("serviceMethodName", "deleteArticle"));
params.add(new BasicNameValuePair("serviceParameterTypes", "[long,java.lang.String,java.lang.String,com.liferay.portal.service.ServiceContext]"));
params.add(new BasicNameValuePair("serviceParameters", "[groupId,articleId,articleURL,serviceContext]"));
params.add(new BasicNameValuePair("groupId", "10156"));
params.add(new BasicNameValuePair("articleId", "60000"));
params.add(new BasicNameValuePair("articleURL", "articleURL"));
params.add(new BasicNameValuePair("serviceContext", "{}"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity);
HttpResponse resp = httpclient.execute(targetHost, post, ctx);
System.out.println(resp.getStatusLine());
resp.getEntity().writeTo(System.out);
httpclient.getConnectionManager().shutdown();
}
More Notes:
- I specified “” (blank) strings for the structureId and templateId. This means that the article has no structure and no template, so it will just be interpreted as raw HTML (as though you had created a new Web Content Article and didn’t specify a structure or template).
- The content parameter is XML with the content inside of a CDATA block. If there were an associated structure for this content, the content would look different. That’s for an advanced reader to explore.
- The timestamps use Java’s Calendar method to set the display date and other dates.
- The articleId is hard-coded to 60000. If you wish to auto-generate specify
autoArticleId
to be true
.
- The
serviceContext
parameter is special and tricky and not well documented. The “content” of the parameter is a JSON-serialized instance of the com.liferay.portal.service.ServiceContext
class. For other services, it might require a more complex serialized instance of the ServiceContext class. For example, instead of {}
(which, which decoded, results in a simple new instance of the ServiceContext class with null
/empty/blank/0 for all of its properties), one might have something like {scopeGroupId:themeDisplay.getScopeGroupId()}
(I took this from this example). Anyway, for this example, an empty {}
works.
If addArticle
works, you should get in return a serialized version of the new article:
{"urlTitle":"test-json-article","indexable":true,"statusDate":"","type":"general","smallImageId":14540,"articleId":"60000","version":1,"id":14538,"title":"Test JSON Article","description":"Test JSON Description","userId":10168,"userName":"Test Test","smallImage":false,"createDate":"1307729885333","displayDate":"1310221080000","smallImageURL":"","expirationDate":"1310912280000","status":2,"statusByUserName":"","reviewDate":"1310912280000","modifiedDate":"1307729885333","content":"<?xml version='1.0' encoding='UTF-8'?><root available-locales=\"en_US\" default-locale=\"en_US\"><static-content language-id=\"en_US\"><![CDATA[<p>\n\ttest content<\/p>]]><\/static-content><\/root>","templateId":"","groupId":10156,"resourcePrimKey":14539,"structureId":"","statusByUserId":0,"companyId":10131,"uuid":"d2a09ad8-43d5-476b-bcb9-3a1621409835"}
Since deleteArticle
returns void
, you won’t get anything (But the article should be gone, which you can verify in the GUI, or via a database browser).