/*
* � Copyright IBM Corp. 2010
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.ibm.domino.services.rest.das.view;
import static com.ibm.domino.services.HttpServiceConstants.*;
import static com.ibm.domino.services.ResponseCode.OK;
import static com.ibm.domino.services.ResponseCode.RSRC_CREATED;
import static com.ibm.domino.services.ResponseCode.RSRC_NOT_FOUND;
import static com.ibm.domino.services.rest.RestParameterConstants.*;
import static com.ibm.domino.services.rest.RestServiceConstants.ITEM_FORM;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.NotesException;
import com.ibm.commons.util.StringUtil;
import com.ibm.commons.util.io.json.JsonException;
import com.ibm.commons.util.io.json.JsonJavaFactory;
import com.ibm.commons.util.io.json.JsonJavaObject;
import com.ibm.commons.util.io.json.JsonParser;
import com.ibm.domino.services.Loggers;
import com.ibm.domino.services.ResponseCode;
import com.ibm.domino.services.ServiceException;
import com.ibm.domino.services.content.DefaultJsonContentFactory;
import com.ibm.domino.services.content.JsonContentFactory;
import com.ibm.domino.services.content.JsonViewDesignContent;
import com.ibm.domino.services.content.JsonViewEntryCollectionContent;
import com.ibm.domino.services.rest.RestServiceOperationHandler;
import com.ibm.domino.services.rest.das.RestDocumentNavigator;
import com.ibm.domino.services.rest.das.RestDocumentNavigatorFactory;
import com.ibm.domino.services.util.JsonWriter;
/**
* Domino JSON View Service.
* This service is compliant with the dojox.JsonRest data store.
*/
public class RestViewJsonService extends RestViewService {
static public final int POST = 0;
static public final int PUT = 1;
static public final int DELETE = 2;
public static final String FOLDER_OP_ADD = "add"; //$NON-NLS-1$
public static final String FOLDER_OP_REMOVE = "remove"; //$NON-NLS-1$
// Indicated if the attributes should be written when they have a default value
// for example, when a count==0
protected boolean forceDefaultAttributes;
private JsonContentFactory factory = DefaultJsonContentFactory.get();
public RestViewJsonService(HttpServletRequest httpRequest, HttpServletResponse httpResponse, ViewParameters parameters) {
super(httpRequest, httpResponse,parameters);
}
/**
* Constructs a <code>RestViewJsonService</code> object.
*
* <p>Use this constructor if you want the service to use a subclass
* of <code>JsonViewEntryCollectionContent</code>. You must implement
* a factory that creates the desired subclass of
* <code>JsonViewEntryCollectionContent</code>.
*
* @param httpRequest The HTTP request.
* @param httpResponse The HTTP response.
* @param parameters View parameters (perhaps parsed from a URL).
* @param factory The factory the service should use to create
* an instance of <code>JsonViewEntryCollectionContent</code>.
*/
public RestViewJsonService(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
ViewParameters parameters, JsonContentFactory factory) {
super(httpRequest, httpResponse,parameters);
if ( factory != null ) {
this.factory = factory;
}
}
@Override
public void renderService() throws ServiceException {
String method = getHttpRequest().getMethod();
if (HTTP_GET.equalsIgnoreCase(method)) {
renderServiceJSONGet();
} else if (HTTP_POST.equalsIgnoreCase(method)) {
String override = getHttpRequest().getHeader(HEADER_X_HTTP_METHOD_OVERRIDE);
if (HTTP_PUT.equalsIgnoreCase(override)) {
renderServiceJSONUpdate(PUT);
} else if (HTTP_DELETE.equalsIgnoreCase(override)) {
renderServiceJSONUpdate(DELETE);
} else {
renderServiceJSONUpdate(POST);
}
} else if (HTTP_PUT.equalsIgnoreCase(method)) {
renderServiceJSONUpdate(PUT);
} else if (HTTP_DELETE.equalsIgnoreCase(method)) {
renderServiceJSONUpdate(DELETE);
} else {
throw new ServiceException(null, ResponseCode.METHOD_NOT_ALLOWED, "Method {0} is not allowed with JSON Rest Service", method); // $NLX-RestViewJsonService.Method0isnotallowedwithJSONRestSe-1$
}
}
// ==========================================================================
// Access to the parameters from the request
// ==========================================================================
@Override
protected ViewParameters wrapViewParameters(ViewParameters parameters) {
return new RequestViewParameter(parameters);
}
protected class RequestViewParameter extends ViewParametersDelegate {
private boolean ignoreRequestParams;
private int start;
private int count;
protected RequestViewParameter(ViewParameters delegate) {
super(delegate);
this.ignoreRequestParams = delegate.isIgnoreRequestParams();
// Header submitted by the client:
// Range: items=0-24
String range=getHttpRequest().getHeader(HEADER_RANGE);
if(StringUtil.isNotEmpty(range) && range.startsWith(HEADER_RANGE_ITEMS)) {
int pos = HEADER_RANGE_ITEMS.length();
int sep = range.indexOf('-',pos);
start = Integer.valueOf(range.substring(pos,sep));
int last = Integer.valueOf(range.substring(sep+1));
count = last-start+1;
} else {
// If the header does not contain range information, we still look at the url.
// Currently the plan is to support both start and count and page, ps and si.
String param = getHttpRequest().getParameter(PARAM_VIEW_START);
if (StringUtil.isNotEmpty(param)) {
try {
start = Integer.parseInt(param);
} catch (NumberFormatException nfe) {}
} else {
start = delegate.getStart();
}
param = getHttpRequest().getParameter(PARAM_VIEW_COUNT);
if (StringUtil.isNotEmpty(param)) {
try {
count = Integer.parseInt(param);
} catch (NumberFormatException nfe) {}
} else {
count = delegate.getCount();
}
// The following three parameters page, ps and si map to start and count.
// count = ps
// start = ps * page + si
param = getHttpRequest().getParameter(PARAM_VIEW_PAGESIZE);
if (StringUtil.isNotEmpty(param)) {
try {
count = Integer.parseInt(param);
} catch (NumberFormatException nfe) {}
} /*else {
count = delegate.getPageSize();
}*/
param = getHttpRequest().getParameter(PARAM_VIEW_PAGEINDEX);
if (StringUtil.isNotEmpty(param)) {
try {
int page = Integer.parseInt(param);
//if (page > 0)
// page-=1;
start = page * count;
} catch (NumberFormatException nfe) {}
} /*else {
start = delegate.getPageIndex();
} */
param = getHttpRequest().getParameter(PARAM_VIEW_STARTINDEX);
if (StringUtil.isNotEmpty(param)) {
try {
int si = Integer.parseInt(param);
start += si;
} catch (NumberFormatException nfe) {}
} /*else {
start = delegate.getStartIndex();
}*/
}
}
@Override
public boolean isIgnoreRequestParams() {
return ignoreRequestParams;
}
// Header returned by the server
// Content-Range: items 0-24/66
@Override
public int getStart() {
return start;
}
@Override
public int getCount() {
return count;
}
@Override
public String getSortColumn() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_SORTCOLUMN);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getSortColumn();
}
@Override
public String getSortOrder() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_SORTORDER);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getSortOrder();
}
@Override
public String getCategoryFilter() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_CATEGORY);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getCategoryFilter();
}
@Override
public int getExpandLevel() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_EXPANDLEVEL);
if (StringUtil.isNotEmpty(param)) {
try {
return Integer.parseInt(param);
} catch (NumberFormatException nfe) {}
}
}
return super.getExpandLevel();
}
@Override
public String getSearch() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_SEARCH);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getSearch();
}
@Override
public Object getStartKeys() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_STARTKEYS);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getStartKeys();
}
@Override
public Object getKeys() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_KEYS);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getKeys();
}
@Override
public String getParentId() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_PARENTID);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getParentId();
}
@Override
public int getSearchMaxDocs() throws ServiceException {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_SEARCHMAXDOCS);
if (StringUtil.isNotEmpty(param)) {
try {
return Integer.parseInt(param);
} catch (NumberFormatException nfe) {
throw new ServiceException(nfe, ResponseCode.BAD_REQUEST, "Invalid max parameter"); // $NLX-RestViewJsonService.Invalidmaxparameter-1$
}
}
}
return super.getSearchMaxDocs();
}
@Override
public boolean isCompact() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_COMPACT);
if (StringUtil.isNotEmpty(param)) {
return param.contentEquals(PARAM_VALUE_TRUE);
}
}
return super.isCompact();
}
@Override
public boolean isKeysExactMatch() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_KEYSEXACTMATCH);
if (StringUtil.isNotEmpty(param)) {
return param.contentEquals(PARAM_VALUE_TRUE);
}
}
return super.isKeysExactMatch();
}
@Override
public boolean isComputeWithForm() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_COMPUTEWITHFORM);
if (StringUtil.isNotEmpty(param)) {
return param.contentEquals(PARAM_VALUE_TRUE);
}
}
return super.isComputeWithForm();
}
@Override
public String getFormName() {
if(!isIgnoreRequestParams()) {
String param = getHttpRequest().getParameter(PARAM_VIEW_FORM);
if (StringUtil.isNotEmpty(param)) {
return param;
}
}
return super.getFormName();
}
}
// ==========================================================================
// GET: read the data
// ==========================================================================
protected void renderServiceJSONGet() throws ServiceException {
try {
ViewParameters parameters = getParameters();
String contentType = parameters.getContentType();
if(StringUtil.isEmpty(contentType)) {
contentType = CONTENTTYPE_APPLICATION_JSON;
}
getHttpResponse().setContentType(contentType);
getHttpResponse().setCharacterEncoding(ENCODING_UTF8);
Writer writer = new OutputStreamWriter(getOutputStream(),ENCODING_UTF8);
boolean compact = parameters.isCompact();
JsonWriter g = new JsonWriter(writer,compact);
String requestUri = super.getHttpRequest().getRequestURI();
if(!StringUtil.isEmpty(requestUri) && (requestUri.endsWith("/"+PARAM_DESIGN) || requestUri.endsWith("/"+PARAM_DESIGN+"/"))) // $NON-NLS-1$ // $NON-NLS-2$ // $NON-NLS-3$
{
JsonViewDesignContent content = factory.createViewDesignContent(getView());
content.writeViewDesign(g);
}
else
{
JsonViewEntryCollectionContent content = factory.createViewEntryCollectionContent(getView(), this);
String rangeHeader = content.getContentRangeHeader(parameters);
content.writeViewEntryCollection(g, parameters);
getHttpResponse().setHeader(HEADER_CONTENT_RANGE, rangeHeader);
writer.flush();
}
} catch(UnsupportedEncodingException ex) {
throw new ServiceException(ex,""); // $NON-NLS-1$
} catch(IOException ex) {
throw new ServiceException(ex,""); // $NON-NLS-1$
} catch (NotesException ex) {
throw new ServiceException(ex,""); // $NON-NLS-1$
}
}
// ==========================================================================
// POST: Create a new document
// ==========================================================================
protected void renderServiceJSONUpdate(int op) throws ServiceException {
ViewParameters parameters = getParameters();
// Read the Json Document
JsonJavaObject json = null;
if (op != DELETE) {
// Look if the request seems correct
String reqContentType = getHttpRequest().getContentType();
if(!reqContentType.contains(CONTENTTYPE_APPLICATION_JSON)) {
throw new ServiceException(null,ResponseCode.BAD_REQUEST,"Request does not contains 'application/json' but {0}",reqContentType); // $NLX-RestViewJsonService.Requestdoesnotcontainsapplication-1$
}
JsonJavaFactory factory = JsonJavaFactory.instanceEx;
// Ok, parse the JSON content
try {
Reader r = getHttpRequest().getReader();
try {
json = (JsonJavaObject)JsonParser.fromJson(factory, r);
} finally {
r.close();
}
// check to see if it's a folder PUT operation
if (op == PUT) {
if (isFolderOperation(json)) {
// make sure we are working with a folder
if (getView().isFolder()) {
HandleOperations opsHandler = new HandleOperations(json);
opsHandler.run();
try {
String contentType = parameters.getContentType();
if(StringUtil.isEmpty(contentType)) {
contentType = CONTENTTYPE_APPLICATION_JSON;
}
getHttpResponse().setContentType(contentType);
getHttpResponse().setCharacterEncoding(ENCODING_UTF8);
Writer writer = new OutputStreamWriter(getOutputStream(),ENCODING_UTF8);
writer.write("{}"); // $NON-NLS-1$
writer.flush();
} catch(UnsupportedEncodingException ex) {
throw new ServiceException(ex,""); // $NON-NLS-1$
} catch(IOException ex) {
throw new ServiceException(ex,""); // $NON-NLS-1$
}
return;
} else
throw new ServiceException(null,ResponseCode.BAD_REQUEST,"Attempting folder operation on a view"); //$NLX-RestViewJsonService.Attemptingfolderoperationonaview-1$
}
}
} catch(Exception ex) {
throw new ServiceException(ex,"Error while parsing the JSON content"); // $NLX-RestViewJsonService.ErrorwhileparsingtheJSONcontent-1$
}
}
// Read the doc id being updated
String id = findUpdateId(json);
RestDocumentNavigator docNav = null;
try {
// Get a view navigator to get access to the columns
RestViewNavigator viewNav = RestViewNavigatorFactory.createNavigatorForDesign(RestViewJsonService.this.getView(),parameters);
// Get a document docNav
docNav = RestDocumentNavigatorFactory.createNavigator(this.getView(),getParameters());
processRow(viewNav, docNav, op, id, json);
} catch(Throwable ex) {
if(ex instanceof ServiceException) {
throw (ServiceException)ex;
}
throw new ServiceException(ex,"Error while updating data"); // $NLX-RestViewJsonService.Errorwhileupdatingdata-1$
} finally {
if (docNav != null)
docNav.recycle();
}
try {
String contentType = parameters.getContentType();
if(StringUtil.isEmpty(contentType)) {
contentType = CONTENTTYPE_APPLICATION_JSON;
}
getHttpResponse().setContentType(contentType);
getHttpResponse().setCharacterEncoding(ENCODING_UTF8);
Writer writer = new OutputStreamWriter(getOutputStream(),ENCODING_UTF8);
writer.write("{}"); // $NON-NLS-1$
writer.flush();
} catch(UnsupportedEncodingException ex) {
throw new ServiceException(ex,""); // $NON-NLS-1$
} catch(IOException ex) {
throw new ServiceException(ex,""); // $NON-NLS-1$
}
}
protected String findUpdateId(JsonJavaObject json) {
String pathInfo = getHttpRequest().getPathInfo();
if(StringUtil.isNotEmpty(pathInfo)) {
String id = pathInfo.substring(pathInfo.lastIndexOf("/")+1); // $NON-NLS-1$
return getEntryUNID(id);
}
return null;
}
protected void processRow(RestViewNavigator viewNav, RestDocumentNavigator docNav, int op, String id, JsonJavaObject items) throws ServiceException, JsonException, IOException {
switch(op) {
case POST: createDocument(viewNav, docNav, items); break;
case PUT: updateDocument(viewNav, docNav, id,items); break;
case DELETE: deleteDocument(viewNav, docNav, id, items); break;
}
}
protected void createDocument(RestViewNavigator viewNav, RestDocumentNavigator docNav, JsonJavaObject items) throws ServiceException, JsonException, IOException {
if(!queryNewDocument()) {
throw new ServiceException(null, msgErrorCreatingDocument());
}
docNav.createDocument();
Document doc = docNav.getDocument();
postNewDocument(doc);
JsonViewEntryCollectionContent content = factory.createViewEntryCollectionContent(view, this);
content.updateFields(viewNav, docNav, items);
String form = getParameters().getFormName();
if (StringUtil.isNotEmpty(form)) {
docNav.replaceItemValue(ITEM_FORM, form);
}
String parentId = getParameters().getParentId();
if (StringUtil.isNotEmpty(parentId)) {
Document parent = null;
try {
parent = database.getDocumentByUNID(parentId);
docNav.getDocument().makeResponse(parent);
}
catch (NotesException e) {
throw new ServiceException(e, msgErrorCreatingDocument());
} finally {
if ( parent != null ) {
try {
parent.recycle();
} catch (NotesException e) {
if( Loggers.SERVICES_LOGGER.isTraceDebugEnabled() ){
Loggers.SERVICES_LOGGER.traceDebug("Exception thrown when recycling parent.", e); // $NON-NLS-1$
}
}
parent = null;
}
}
}
if (getParameters().isComputeWithForm()) {
docNav.computeWithForm();
}
if(!querySaveDocument(doc)) {
throw new ServiceException(null, msgErrorCreatingDocument());
}
docNav.save();
postSaveDocument(doc);
getHttpResponse().setStatus(RSRC_CREATED.httpStatusCode);
try {
String baseURL = getHttpRequest().getRequestURI();
getHttpResponse().addHeader(HEADER_LOCATION, baseURL + doc.getUniversalID());
}
catch (NotesException e) {
throw new ServiceException(null, msgErrorCreatingDocument()); // $NLX-RestViewJsonService.Errorcreatingdocument-1$
}
}
private String msgErrorCreatingDocument() {
return "Error creating document."; // $NLX-RestViewJsonService.Errorcreatingdocument.3-1$
}
protected void updateDocument(RestViewNavigator viewNav, RestDocumentNavigator docNav, String id, JsonJavaObject items) throws ServiceException, JsonException, IOException {
if(!queryOpenDocument(id)) {
throw new ServiceException(null, msgErrorUpdatingData());
}
docNav.openDocument(id);
Document doc = docNav.getDocument();
postOpenDocument(doc);
JsonViewEntryCollectionContent content = factory.createViewEntryCollectionContent(view, this);
content.updateFields(viewNav, docNav, items);
if (getParameters().isComputeWithForm()) {
docNav.computeWithForm();
}
if(!querySaveDocument(doc)) {
throw new ServiceException(null, msgErrorUpdatingData());
}
docNav.save();
postSaveDocument(doc);
}
private String msgErrorUpdatingData() {
return "Error updating data."; // $NLX-RestViewJsonService.Errorupdatingdata.1-1$
}
protected void deleteDocument(RestViewNavigator viewNav, RestDocumentNavigator docNav, String id, JsonJavaObject items) throws ServiceException, JsonException, IOException {
if(!queryDeleteDocument(id)) {
throw new ServiceException(null, msgErrorDeletingDocument());
}
try {
docNav.deleteDocument(id);
} catch (Exception e) {
throw new ServiceException(e, RSRC_NOT_FOUND, msgErrorDeletingDocument());
}
postDeleteDocument(id);
}
private String msgErrorDeletingDocument() {
return "Error deleting document."; // $NLX-RestViewJsonService.Errordeletingdocument.1-1$
}
private boolean isFolderOperation(JsonJavaObject json) {
boolean folderOp = false;
if ((json.getJsonProperty(FOLDER_OP_ADD) != null) ||
(json.getJsonProperty(FOLDER_OP_REMOVE) != null)) {
folderOp = true;
}
return folderOp;
}
private class HandleOperations extends RestServiceOperationHandler {
private HashMap <String, String> docOperations; // docunid, operation (add/remove)
public HandleOperations(Object content) {
super(content);
this.docOperations = new HashMap<String, String>();
}
@Override
public void run() throws ServiceException {
JsonJavaObject requestBody = (JsonJavaObject) getContent(); // contains the json request info
setupOperations(requestBody);
try {
Database db = getView().getParent();
String folderName = getView().getName();
Document doc = null;
for( Map.Entry<String,String> e: docOperations.entrySet() ) {
String docunid = e.getKey();
boolean add = (e.getValue().equalsIgnoreCase(FOLDER_OP_ADD));
doc = db.getDocumentByUNID(docunid);
if (add)
doc.putInFolder(folderName);
else
doc.removeFromFolder(folderName);
if (doc != null)
doc.recycle();
}
} catch (NotesException e) {
throw new ServiceException(e, "");
} finally {
getHttpResponse().setStatus(OK.httpStatusCode);
}
}
@SuppressWarnings("unchecked") // $NON-NLS-1$
private void setupOperations (JsonJavaObject requestBody) {
for(Iterator<String> it = requestBody.getJsonProperties(); it.hasNext(); ) {
String opName = it.next();
if ((opName.equalsIgnoreCase(FOLDER_OP_ADD)) ||
(opName.equalsIgnoreCase(FOLDER_OP_REMOVE))) {
Object value = requestBody.get(opName);
if (value instanceof List) {
Vector<?> vector = new Vector((List)value);
if (!vector.isEmpty()) {
for(Iterator<?> docs = vector.iterator(); docs.hasNext(); ) {
String docunid = (String) docs.next();
//System.out.println("docunid is = " + docunid);
if (docunid.length() > 0)
docOperations.put(docunid, opName);
}
}
}
}
}
}
}
}