/* * � 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.document; import static com.ibm.domino.services.HttpServiceConstants.CONTENTTYPE_APPLICATION_JSON; import static com.ibm.domino.services.HttpServiceConstants.ENCODING_UTF8; import static com.ibm.domino.services.HttpServiceConstants.HEADER_LOCATION; import static com.ibm.domino.services.HttpServiceConstants.HEADER_X_HTTP_METHOD_OVERRIDE; import static com.ibm.domino.services.HttpServiceConstants.HTTP_DELETE; import static com.ibm.domino.services.HttpServiceConstants.HTTP_GET; import static com.ibm.domino.services.HttpServiceConstants.HTTP_PATCH; import static com.ibm.domino.services.HttpServiceConstants.HTTP_POST; import static com.ibm.domino.services.HttpServiceConstants.HTTP_PUT; import static com.ibm.domino.services.ResponseCode.RSRC_CREATED; import static com.ibm.domino.services.rest.RestParameterConstants.*; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_COMPUTEWITHFORM; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_DOCUMENTID; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_FORM; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_MARKREAD; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_PARENTID; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_SEARCH; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_SEARCHMAXDOCS; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_SINCE; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_DOC_STRONGTYPE; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_SEPERATOR; import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_VALUE_TRUE; 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.List; 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.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.JsonDocumentCollectionContent; import com.ibm.domino.services.content.JsonDocumentContent; import com.ibm.domino.services.util.JsonWriter; /** * Domino Document Service as a JSON object. */ public class RestDocumentJsonService extends RestDocumentService { private JsonContentFactory factory = DefaultJsonContentFactory.get(); public RestDocumentJsonService(HttpServletRequest httpRequest, HttpServletResponse httpResponse, DocumentParameters parameters) { super(httpRequest, httpResponse, parameters); } /** * Constructs a <code>RestDocumentJsonService</code> object. * * <p>Use this constructor if you want the service to use a subclass * of <code>JsonDocumentContent</code>. You must implement * a factory that creates the desired subclass of * <code>JsonDocumentContent</code>. * * @param httpRequest The HTTP request. * @param httpResponse The HTTP response. * @param parameters Document parameters (perhaps parsed from a URL). * @param factory The factory the service should use to create * an instance of <code>JsonDocumentContent</code>. */ public RestDocumentJsonService(HttpServletRequest httpRequest, HttpServletResponse httpResponse, DocumentParameters parameters, JsonContentFactory factory) { super(httpRequest, httpResponse, parameters); if ( factory != null ) { this.factory = factory; } } static public enum Method { POST(HTTP_POST), PUT(HTTP_PUT), DELETE(HTTP_DELETE), GET(HTTP_GET), PATCH(HTTP_PATCH); public final String name; Method(final String name) { this.name = name; } static Method getMethod(final String method) { Method result = null; final String verb = method.toUpperCase(); if (verb.equals(GET.name)) result = Method.GET; else if (verb.equals(PUT.name)) result = Method.PUT; else if (verb.equals(DELETE.name)) result = Method.DELETE; else if (verb.equals(POST.name)) result = Method.POST; else if (verb.equals(PATCH.name)) result = Method.PATCH; return result; } static boolean isMethod(final Method method, final String name) { if (method == null || name == null) return false; return Method.getMethod(name) == method; } boolean isMethod(final Method method) { if (method == null) return false; return this == method; } } @Override public void renderService() throws ServiceException { String method = getHttpRequest().getMethod(); if (Method.isMethod(Method.GET, method )) { renderServiceJSONGet(); } else if (Method.isMethod(Method.POST, method)) { String override = getHttpRequest().getHeader(HEADER_X_HTTP_METHOD_OVERRIDE); if (Method.isMethod(Method.PUT, override)) { renderServiceJSONUpdate(Method.PUT); } else if (Method.isMethod(Method.DELETE, override)) { renderServiceJSONUpdate(Method.DELETE); } else if (Method.isMethod(Method.PATCH, override)) { renderServiceJSONUpdate(Method.PATCH); }else { renderServiceJSONUpdate(Method.POST); } } else if (Method.isMethod(Method.PUT, method)) { renderServiceJSONUpdate(Method.PUT); } else if (Method.isMethod(Method.PATCH, method)) { renderServiceJSONUpdate(Method.PATCH); } else if (HTTP_DELETE.equalsIgnoreCase(method)) { renderServiceJSONUpdate(Method.DELETE); } else { throw new ServiceException(null, ResponseCode.METHOD_NOT_ALLOWED, "Method {0} is not allowed with JSON Rest Service", method); // $NLX-RestDocumentJsonService.Method0isnotallowedwithJSONRestSe-1$ } } // ========================================================================== // Access to the parameters from the request // ========================================================================== @Override protected DocumentParameters wrapDocumentParameters(DocumentParameters parameters) { return new RequestDocumentParameter(parameters); } protected class RequestDocumentParameter extends DocumentParametersDelegate { private boolean ignoreRequestParams; protected RequestDocumentParameter(DocumentParameters delegate) { super(delegate); this.ignoreRequestParams = delegate.isIgnoreRequestParams(); } @Override public boolean isIgnoreRequestParams() { return ignoreRequestParams; } @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 String getDocumentUnid() { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_DOCUMENTID); if (StringUtil.isNotEmpty(param)) { return param; } } return super.getDocumentUnid(); } @Override public String getParentId() { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_PARENTID); if (StringUtil.isNotEmpty(param)) { return param; } } return super.getParentId(); } @Override public boolean isComputeWithForm() { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_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_DOC_FORM); if (StringUtil.isNotEmpty(param)) { return param; } } return super.getFormName(); } @Override public boolean isMarkRead() { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_MARKREAD); if (StringUtil.isNotEmpty(param)) { return param.contentEquals(PARAM_VALUE_TRUE); } } return super.isMarkRead(); } @Override public String getSince() { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_SINCE); if (StringUtil.isNotEmpty(param)) { return param; } } return super.getSince(); } @Override public String getSearch() { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_SEARCH); if (StringUtil.isNotEmpty(param)) { return param; } } return super.getSearch(); } @Override public int getSearchMaxDocs() throws ServiceException { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_SEARCHMAXDOCS); if (StringUtil.isNotEmpty(param)) { try { return Integer.parseInt(param); } catch (NumberFormatException nfe) { throw new ServiceException(nfe, ResponseCode.BAD_REQUEST, "Invalid max parameter"); // $NLX-RestDocumentJsonService.Invalidmaxparameter-1$ } } } return super.getSearchMaxDocs(); } @Override public boolean isStrongType() { if(!isIgnoreRequestParams()) { String param = getHttpRequest().getParameter(PARAM_DOC_STRONGTYPE); if (StringUtil.isNotEmpty(param)) { return param.contentEquals(PARAM_VALUE_TRUE); } } return super.isStrongType(); } } // ========================================================================== // GET: read the data // ========================================================================== protected void renderServiceJSONGet() throws ServiceException { try { DocumentParameters 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 jsonWriter = new JsonWriter(writer,compact); try { int global = parameters.getGlobalValues(); if((global & DocumentParameters.GLOBAL_TIMESTAMP)!=0) { // Should go to the HTTP header // jsonWriter.startProperty("@timestamp"); // jsonWriter.outDateLiteral(new Date()); // jsonWriter.endProperty(); } if((global & DocumentParameters.GLOBAL_ENTRIES)!=0) { boolean defItems = parameters.isDefaultItems(); int sysItems = parameters.getSystemItems(); if (getDocument() == null) { Database database = this.getDatabase(getParameters()); String uri = this.getHttpRequest().getRequestURI() + PARAM_SEPERATOR; String search = getParameters().getSearch(); String since = getParameters().getSince(); int max = getParameters().getSearchMaxDocs(); JsonDocumentCollectionContent content = factory.createDocumentCollectionContent(database, uri, search, since, max); content.writeDocumentCollection(jsonWriter); } else { boolean strongtype = getParameters().isStrongType(); String rtType = null; List<RestDocumentItem> custItems = getParameters().getItems(); String id; try { id = getDocumentUnid(); } catch (NotesException ex) { throw new ServiceException(ex, msgErrorGettingDocument()); } if(!queryOpenDocument(id)) { throw new ServiceException(null, msgErrorGettingDocument()); } Document document; try { document = this.getDatabase(getParameters()).getDocumentByUNID(id); } catch (NotesException ex) { throw new ServiceException(ex, msgErrorGettingDocument()); } postOpenDocument(document); JsonDocumentContent content = factory.createDocumentContent(document, this); // TODO: Add rtType parameter; content.writeDocumentAsJson(jsonWriter, sysItems, defItems, custItems, strongtype, rtType, null); if (getParameters().isMarkRead()) { document.markRead(); } } } } catch (NotesException ex) { throw new ServiceException(ex,""); // $NON-NLS-1$ } finally { writer.flush(); } } catch(UnsupportedEncodingException ex) { throw new ServiceException(ex,""); // $NON-NLS-1$ } catch(IOException ex) { throw new ServiceException(ex,""); // $NON-NLS-1$ } } private String msgErrorGettingDocument() { // extracted to a method to prevent re-translation for every occurance return "Error getting document."; // $NLX-RestDocumentJsonService.Errorgettingdocument-1$ } protected void renderServiceJSONUpdate(Method method) throws ServiceException { DocumentParameters parameters = getParameters(); // Parse the JSON content if needed. JsonJavaObject json = null; if (!method.isMethod(Method.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-RestDocumentJsonService.Requestdoesnotcontainsapplication-1$ } JsonJavaFactory factory = JsonJavaFactory.instanceEx; try { Reader r = getHttpRequest().getReader(); try { json = (JsonJavaObject)JsonParser.fromJson(factory, r); } finally { r.close(); } } catch(Exception ex) { throw new ServiceException(ex, "Error while parsing the JSON content"); // $NLX-RestDocumentJsonService.ErrorwhileparsingtheJSONcontent-1$ } } // Process the request. switch(method) { case POST: createDocument(json); break; case PUT: updateDocument(json, true); break; case PATCH: updateDocument(json, false); break; case DELETE: deleteDocument(); break; } // Set the response. 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 void createDocument(JsonJavaObject items) throws ServiceException { if(!queryNewDocument()) { throw new ServiceException(null, msgErrorCreatingDocument()); } Document document; try { document = this.getDatabase(getParameters()).createDocument(); } catch (NotesException ex) { throw new ServiceException(ex, msgErrorCreatingDocument()); } postNewDocument(document); try { JsonDocumentContent content = factory.createDocumentContent(document, this); String form = getParameters().getFormName(); if (StringUtil.isNotEmpty(form)) { document.replaceItemValue(ITEM_FORM, form); } content.updateFields(items, false); String parentId = getParameters().getParentId(); if (StringUtil.isNotEmpty(parentId)) { Document parent = null; try { parent = database.getDocumentByUNID(parentId); document.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.traceDebugp(this, "createDocument", e, // $NON-NLS-1$ "Exception thrown when recycling parent.");// $NON-NLS-1$ } } parent = null; } } } if (getParameters().isComputeWithForm()) { document.computeWithForm(true, true); } if(!querySaveDocument(document)) { throw new ServiceException(null, msgErrorCreatingDocument()); } document.save(); postSaveDocument(document); getHttpResponse().setStatus(RSRC_CREATED.httpStatusCode); try { String baseURL = getHttpRequest().getRequestURI(); getHttpResponse().addHeader(HEADER_LOCATION, baseURL + document.getUniversalID()); } catch (NotesException e) { throw new ServiceException(null, msgErrorCreatingDocument()); } } catch (NotesException ex) { throw new ServiceException(ex, msgErrorCreatingDocument()); } finally { if(document != null) { try { document.recycle(); } catch(NotesException ex) { if( Loggers.SERVICES_LOGGER.isTraceDebugEnabled() ){ Loggers.SERVICES_LOGGER.traceDebugp(this, "createDocument", ex, // $NON-NLS-1$ "Exception thrown on recycle."); // $NON-NLS-1$ } } } } } /** * @return */ private String msgErrorCreatingDocument() { // extracted to a method to prevent re-translation for every occurance return "Error creating document.";// $NLX-RestDocumentJsonService.Errorcreatingdocument-1$ } /** * HTTP PUT method only allows a complete replacement of a document. * This proposal adds a new HTTP method, PATCH, to modify an existing HTTP resource. * * http://tools.ietf.org/html/rfc5789 */ protected void updateDocument(JsonJavaObject items, boolean put) throws ServiceException { String id; try { id = getDocumentUnid(); } catch (NotesException ex) { throw new ServiceException(ex, msgErrorUpdatingDocument()); } if(!queryOpenDocument(id)) { throw new ServiceException(null, msgErrorUpdatingDocument()); } Document document; try { document = this.getDatabase(getParameters()).getDocumentByUNID(id); } catch (NotesException ex) { throw new ServiceException(ex, msgErrorUpdatingDocument()); } postOpenDocument(document); try { String form = getParameters().getFormName(); if (StringUtil.isNotEmpty(form)) { document.replaceItemValue(ITEM_FORM, form); } JsonDocumentContent content = factory.createDocumentContent(document, this); content.updateFields(items, put); if (getParameters().isComputeWithForm()) { document.computeWithForm(true, true); } if(!querySaveDocument(document)) { throw new ServiceException(null, msgErrorUpdatingDocument()); } document.save(); postSaveDocument(document); } catch (NotesException ex) { throw new ServiceException(ex, msgErrorUpdatingDocument()); } finally { if(document != null) { try { document.recycle(); } catch(NotesException ex) { if( Loggers.SERVICES_LOGGER.isTraceDebugEnabled() ){ Loggers.SERVICES_LOGGER.traceDebugp(this, "updateDocument", ex, // $NON-NLS-1$ "Exception thrown on recycle.");// $NON-NLS-1$ } } document = null; } } } /** * @return */ private String msgErrorUpdatingDocument() { // extracted to a method to prevent re-translation for every occurance return "Error updating document."; // $NLX-RestDocumentJsonService.Errorupdatingdocument-1$ } protected void deleteDocument() throws ServiceException { String id; try { id = getDocumentUnid(); } catch (NotesException ex) { throw new ServiceException(ex, msgErrorDeletingDocument()); } if(!queryDeleteDocument(id)) { throw new ServiceException(null, msgErrorDeletingDocument()); } try { Document document = this.getDatabase(getParameters()).getDocumentByUNID(id); if(!document.remove(true)) { throw new ServiceException(null, "Document is not deleted because another user modified it."); // $NLX-RestDocumentJsonService.Documentisnotdeletedbecauseanothe-1$ } } catch (NotesException ex) { throw new ServiceException(ex, msgErrorDeletingDocument()); } postDeleteDocument(id); } /** * @return */ private String msgErrorDeletingDocument() { // extracted to a method to prevent re-translation for every occurance return "Error deleting document."; // $NLX-RestDocumentJsonService.Errordeletingdocument-1$ } }