/**
* Copyright (C) 2010-2014 Morgner UG (haftungsbeschränkt)
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.rest.servlet;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import java.io.IOException;
import java.io.Writer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.neo4j.kernel.DeadlockDetectedException;
import org.structr.common.PagingHelper;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.core.*;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.auth.Authenticator;
import org.structr.core.entity.AbstractNode;
import org.structr.core.graph.NodeFactory;
import org.structr.core.graph.Tx;
import org.structr.core.graph.search.SearchCommand;
import org.structr.core.property.PropertyKey;
import org.structr.rest.JsonInputGSONAdapter;
import org.structr.rest.RestMethodResult;
import org.structr.rest.adapter.FrameworkExceptionGSONAdapter;
import org.structr.rest.adapter.ResultGSONAdapter;
import org.structr.rest.resource.Resource;
import org.structr.rest.serialization.StreamingHtmlWriter;
import org.structr.rest.serialization.StreamingJsonWriter;
import org.structr.rest.service.HttpService;
import org.structr.rest.service.HttpServiceServlet;
import org.structr.rest.service.StructrHttpServiceConfig;
import org.tuckey.web.filters.urlrewrite.utils.StringUtils;
//~--- classes ----------------------------------------------------------------
/**
* Implements the structr REST API.
*
* @author Christian Morgner
*/
public class JsonRestServlet extends HttpServlet implements HttpServiceServlet {
public static final int DEFAULT_VALUE_PAGE_SIZE = 20;
public static final String DEFAULT_VALUE_SORT_ORDER = "asc";
public static final String REQUEST_PARAMETER_LOOSE_SEARCH = "loose";
public static final String REQUEST_PARAMETER_PAGE_NUMBER = "page";
public static final String REQUEST_PARAMETER_PAGE_SIZE = "pageSize";
public static final String REQUEST_PARAMETER_OFFSET_ID = "pageStartId";
public static final String REQUEST_PARAMETER_SORT_KEY = "sort";
public static final String REQUEST_PARAMETER_SORT_ORDER = "order";
public static final Set<String> commonRequestParameters = new LinkedHashSet<>();
private static final Logger logger = Logger.getLogger(JsonRestServlet.class.getName());
static {
commonRequestParameters.add(REQUEST_PARAMETER_LOOSE_SEARCH);
commonRequestParameters.add(REQUEST_PARAMETER_PAGE_NUMBER);
commonRequestParameters.add(REQUEST_PARAMETER_PAGE_SIZE);
commonRequestParameters.add(REQUEST_PARAMETER_OFFSET_ID);
commonRequestParameters.add(REQUEST_PARAMETER_SORT_KEY);
commonRequestParameters.add(REQUEST_PARAMETER_SORT_ORDER);
// cross reference here, but these need to be added as well..
commonRequestParameters.add(SearchCommand.DISTANCE_SEARCH_KEYWORD);
commonRequestParameters.add(SearchCommand.LOCATION_SEARCH_KEYWORD);
commonRequestParameters.add(SearchCommand.STREET_SEARCH_KEYWORD);
commonRequestParameters.add(SearchCommand.HOUSE_SEARCH_KEYWORD);
commonRequestParameters.add(SearchCommand.POSTAL_CODE_SEARCH_KEYWORD);
commonRequestParameters.add(SearchCommand.CITY_SEARCH_KEYWORD);
commonRequestParameters.add(SearchCommand.STATE_SEARCH_KEYWORD);
commonRequestParameters.add(SearchCommand.COUNTRY_SEARCH_KEYWORD);
}
// final fields
private final Map<Pattern, Class<? extends Resource>> resourceMap = new LinkedHashMap<>();
private final StructrHttpServiceConfig config = new StructrHttpServiceConfig();
// non-final fields
private Value<String> propertyView = null;
private ThreadLocalGson gson = null;
private Writer logWriter = null;
private boolean indentJson = true;
//~--- methods --------------------------------------------------------
@Override
public StructrHttpServiceConfig getConfig() {
return config;
}
@Override
public void init() {
try {
indentJson = Boolean.parseBoolean(StructrApp.getConfigurationValue(Services.JSON_INDENTATION, "true"));
} catch (Throwable t) {
logger.log(Level.WARNING, "Unable to parse value for {0}: {1}", new Object[] { Services.JSON_INDENTATION, t.getMessage() } );
}
// inject resources
resourceMap.putAll(config.getResourceProvider().getResources());
// initialize variables
this.propertyView = new ThreadLocalPropertyView();
this.gson = new ThreadLocalGson(config.getOutputNestingDepth());
}
@Override
public void destroy() {
if (logWriter != null) {
try {
logWriter.flush();
logWriter.close();
} catch (IOException ioex) {
logger.log(Level.WARNING, "Could not close access log file.", ioex);
}
}
}
// <editor-fold defaultstate="collapsed" desc="DELETE">
@Override
protected void doDelete(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
SecurityContext securityContext = null;
Authenticator authenticator = null;
RestMethodResult result = null;
Resource resource = null;
try {
// first thing to do!
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// isolate request authentication in a transaction
try (final Tx tx = StructrApp.getInstance().tx()) {
authenticator = config.getAuthenticator();
securityContext = authenticator.initializeAndExamineRequest(request, response);
tx.success();
}
final App app = StructrApp.getInstance(securityContext);
// isolate resource authentication
try (final Tx tx = app.tx()) {
resource = ResourceHelper.optimizeNestedResourceChain(ResourceHelper.parsePath(securityContext, request, resourceMap, propertyView, config.getDefaultIdProperty()), config.getDefaultIdProperty());
authenticator.checkResourceAccess(request, resource.getResourceSignature(), propertyView.get(securityContext));
tx.success();
}
// isolate doDelete
boolean retry = true;
while (retry) {
try (final Tx tx = app.tx()) {
result = resource.doDelete();
tx.success();
retry = false;
} catch (DeadlockDetectedException ddex) {
retry = true;
}
}
// isolate write output
try (final Tx tx = app.tx()) {
result.commitResponse(gson.get(), response);
tx.success();
}
} catch (FrameworkException frameworkException) {
// set status & write JSON output
response.setStatus(frameworkException.getStatus());
gson.get().toJson(frameworkException, response.getWriter());
response.getWriter().println();
} catch (JsonSyntaxException jsex) {
logger.log(Level.WARNING, "JsonSyntaxException in DELETE", jsex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in DELETE: " + jsex.getMessage()));
} catch (JsonParseException jpex) {
logger.log(Level.WARNING, "JsonParseException in DELETE", jpex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in DELETE: " + jpex.getMessage()));
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception in DELETE", t);
int code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in DELETE: " + t.getMessage()));
} finally {
try {
//response.getWriter().flush();
response.getWriter().close();
} catch (IOException t) {
logger.log(Level.WARNING, "Unable to flush and close response: {0}", t.getMessage());
}
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="GET">
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
SecurityContext securityContext = null;
Authenticator authenticator = null;
Result result = null;
Resource resource = null;
try {
// first thing to do!
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// isolate request authentication in a transaction
try (final Tx tx = StructrApp.getInstance().tx()) {
authenticator = config.getAuthenticator();
securityContext = authenticator.initializeAndExamineRequest(request, response);
tx.success();
}
final App app = StructrApp.getInstance(securityContext);
// set default value for property view
propertyView.set(securityContext, config.getDefaultPropertyView());
// evaluate constraints and measure query time
double queryTimeStart = System.nanoTime();
// isolate resource authentication
try (final Tx tx = app.tx()) {
resource = ResourceHelper.applyViewTransformation(request, securityContext,
ResourceHelper.optimizeNestedResourceChain(
ResourceHelper.parsePath(securityContext, request, resourceMap, propertyView,
config.getDefaultIdProperty()), config.getDefaultIdProperty()), propertyView);
authenticator.checkResourceAccess(request, resource.getResourceSignature(), propertyView.get(securityContext));
tx.success();
}
// add sorting & paging
String pageSizeParameter = request.getParameter(REQUEST_PARAMETER_PAGE_SIZE);
String pageParameter = request.getParameter(REQUEST_PARAMETER_PAGE_NUMBER);
String offsetId = request.getParameter(REQUEST_PARAMETER_OFFSET_ID);
String sortOrder = request.getParameter(REQUEST_PARAMETER_SORT_ORDER);
String sortKeyName = request.getParameter(REQUEST_PARAMETER_SORT_KEY);
boolean sortDescending = (sortOrder != null && "desc".equals(sortOrder.toLowerCase()));
int pageSize = HttpService.parseInt(pageSizeParameter, NodeFactory.DEFAULT_PAGE_SIZE);
int page = HttpService.parseInt(pageParameter, NodeFactory.DEFAULT_PAGE);
String baseUrl = request.getRequestURI();
PropertyKey sortKey = null;
// set sort key
if (sortKeyName != null) {
Class<? extends GraphObject> type = resource.getEntityClass();
if (type == null) {
// fallback to default implementation
// if no type can be determined
type = AbstractNode.class;
}
sortKey = StructrApp.getConfiguration().getPropertyKeyForDatabaseName(type, sortKeyName);
}
// isolate doGet
boolean retry = true;
while (retry) {
try (final Tx tx = app.tx()) {
result = resource.doGet(sortKey, sortDescending, pageSize, page, offsetId);
tx.success();
retry = false;
} catch (DeadlockDetectedException ddex) {
retry = true;
}
}
result.setIsCollection(resource.isCollectionResource());
result.setIsPrimitiveArray(resource.isPrimitiveArray());
PagingHelper.addPagingParameter(result, pageSize, page);
// timing..
double queryTimeEnd = System.nanoTime();
// store property view that will be used to render the results
result.setPropertyView(propertyView.get(securityContext));
// allow resource to modify result set
resource.postProcessResultSet(result);
DecimalFormat decimalFormat = new DecimalFormat("0.000000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
result.setQueryTime(decimalFormat.format((queryTimeEnd - queryTimeStart) / 1000000000.0));
String accept = request.getHeader("Accept");
if (accept != null && accept.contains("text/html")) {
final StreamingHtmlWriter htmlStreamer = new StreamingHtmlWriter(this.propertyView, indentJson, config.getOutputNestingDepth());
// isolate write output
try (final Tx tx = app.tx()) {
response.setContentType("text/html; charset=utf-8");
try (final Writer writer = response.getWriter()) {
htmlStreamer.stream(securityContext, writer, result, baseUrl);
writer.append("\n"); // useful newline
}
tx.success();
}
} else {
final StreamingJsonWriter jsonStreamer = new StreamingJsonWriter(this.propertyView, indentJson, config.getOutputNestingDepth());
// isolate write output
try (final Tx tx = app.tx()) {
response.setContentType("application/json; charset=utf-8");
try (final Writer writer = response.getWriter()) {
jsonStreamer.stream(securityContext, writer, result, baseUrl);
writer.append("\n"); // useful newline
}
tx.success();
}
}
response.setStatus(HttpServletResponse.SC_OK);
} catch (FrameworkException frameworkException) {
// set status & write JSON output
response.setStatus(frameworkException.getStatus());
gson.get().toJson(frameworkException, response.getWriter());
response.getWriter().println();
} catch (JsonSyntaxException jsex) {
logger.log(Level.WARNING, "JsonSyntaxException in GET", jsex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "Json syntax exception in GET: " + jsex.getMessage()));
} catch (JsonParseException jpex) {
logger.log(Level.WARNING, "JsonParseException in GET", jpex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "Parser exception in GET: " + jpex.getMessage()));
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception in GET", t);
int code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "Exception in GET: " + t.getMessage()));
} finally {
try {
//response.getWriter().flush();
response.getWriter().close();
} catch (Throwable t) {
logger.log(Level.WARNING, "Unable to flush and close response: {0}", t.getMessage());
}
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="HEAD">
@Override
protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
SecurityContext securityContext = null;
Authenticator authenticator = null;
Resource resource = null;
try {
// first thing to do!
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// isolate request authentication in a transaction
try (final Tx tx = StructrApp.getInstance().tx()) {
authenticator = config.getAuthenticator();
securityContext = authenticator.initializeAndExamineRequest(request, response);
tx.success();
}
final App app = StructrApp.getInstance(securityContext);
// set default value for property view
propertyView.set(securityContext, config.getDefaultPropertyView());
// isolate resource authentication
try (final Tx tx = app.tx()) {
resource = ResourceHelper.applyViewTransformation(request, securityContext,
ResourceHelper.optimizeNestedResourceChain(
ResourceHelper.parsePath(securityContext, request, resourceMap, propertyView,
config.getDefaultIdProperty()), config.getDefaultIdProperty()), propertyView);
authenticator.checkResourceAccess(request, resource.getResourceSignature(), propertyView.get(securityContext));
tx.success();
}
// add sorting & paging
String pageSizeParameter = request.getParameter(REQUEST_PARAMETER_PAGE_SIZE);
String pageParameter = request.getParameter(REQUEST_PARAMETER_PAGE_NUMBER);
String offsetId = request.getParameter(REQUEST_PARAMETER_OFFSET_ID);
String sortOrder = request.getParameter(REQUEST_PARAMETER_SORT_ORDER);
String sortKeyName = request.getParameter(REQUEST_PARAMETER_SORT_KEY);
boolean sortDescending = (sortOrder != null && "desc".equals(sortOrder.toLowerCase()));
int pageSize = HttpService.parseInt(pageSizeParameter, NodeFactory.DEFAULT_PAGE_SIZE);
int page = HttpService.parseInt(pageParameter, NodeFactory.DEFAULT_PAGE);
PropertyKey sortKey = null;
// set sort key
if (sortKeyName != null) {
Class<? extends GraphObject> type = resource.getEntityClass();
if (type == null) {
// fallback to default implementation
// if no type can be determined
type = AbstractNode.class;
}
sortKey = StructrApp.getConfiguration().getPropertyKeyForDatabaseName(type, sortKeyName);
}
// isolate doGet
boolean retry = true;
while (retry) {
try (final Tx tx = app.tx()) {
resource.doGet(sortKey, sortDescending, pageSize, page, offsetId);
tx.success();
retry = false;
} catch (DeadlockDetectedException ddex) {
retry = true;
}
}
response.setStatus(HttpServletResponse.SC_OK);
} catch (FrameworkException frameworkException) {
// set status & write JSON output
response.setStatus(frameworkException.getStatus());
gson.get().toJson(frameworkException, response.getWriter());
response.getWriter().println();
} catch (JsonSyntaxException jsex) {
logger.log(Level.WARNING, "JsonSyntaxException in HEAD", jsex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "Json syntax exception in HEAD: " + jsex.getMessage()));
} catch (JsonParseException jpex) {
logger.log(Level.WARNING, "JsonParseException in HEAD", jpex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "Parser exception in HEAD: " + jpex.getMessage()));
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception in HEAD", t);
int code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "Exception in HEAD: " + t.getMessage()));
} finally {
try {
//response.getWriter().flush();
response.getWriter().close();
} catch (Throwable t) {
logger.log(Level.WARNING, "Unable to flush and close response: {0}", t.getMessage());
}
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="OPTIONS">
@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
SecurityContext securityContext = null;
Authenticator authenticator = null;
RestMethodResult result = null;
Resource resource = null;
try {
// first thing to do!
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// isolate request authentication in a transaction
try (final Tx tx = StructrApp.getInstance().tx()) {
authenticator = config.getAuthenticator();
securityContext = authenticator.initializeAndExamineRequest(request, response);
tx.success();
}
final App app = StructrApp.getInstance(securityContext);
// isolate resource authentication
try (final Tx tx = app.tx()) {
resource = ResourceHelper.applyViewTransformation(request, securityContext,
ResourceHelper.optimizeNestedResourceChain(ResourceHelper.parsePath(securityContext, request, resourceMap, propertyView,
config.getDefaultIdProperty()), config.getDefaultIdProperty()), propertyView);
authenticator.checkResourceAccess(request, resource.getResourceSignature(), propertyView.get(securityContext));
tx.success();
}
// isolate doOptions
boolean retry = true;
while (retry) {
try (final Tx tx = app.tx()) {
result = resource.doOptions();
tx.success();
retry = false;
} catch (DeadlockDetectedException ddex) {
retry = true;
}
}
// isolate write output
try (final Tx tx = app.tx()) {
result.commitResponse(gson.get(), response);
tx.success();
}
} catch (FrameworkException frameworkException) {
// set status & write JSON output
response.setStatus(frameworkException.getStatus());
gson.get().toJson(frameworkException, response.getWriter());
response.getWriter().println();
} catch (JsonSyntaxException jsex) {
logger.log(Level.WARNING, "JsonSyntaxException in OPTIONS", jsex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in OPTIONS: " + jsex.getMessage()));
} catch (JsonParseException jpex) {
logger.log(Level.WARNING, "JsonParseException in OPTIONS", jpex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in OPTIONS: " + jpex.getMessage()));
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception in OPTIONS", t);
int code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in OPTIONS: " + t.getMessage()));
} finally {
try {
//response.getWriter().flush();
response.getWriter().close();
} catch (Throwable t) {
logger.log(Level.WARNING, "Unable to flush and close response: {0}", t.getMessage());
}
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="POST">
@Override
protected void doPost(final HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
final List<RestMethodResult> results = new LinkedList<>();
SecurityContext securityContext = null;
Authenticator authenticator = null;
IJsonInput jsonInput = null;
Resource resource = null;
try {
// first thing to do!
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// isolate request authentication in a transaction
try (final Tx tx = StructrApp.getInstance().tx()) {
authenticator = config.getAuthenticator();
securityContext = authenticator.initializeAndExamineRequest(request, response);
tx.success();
}
final App app = StructrApp.getInstance(securityContext);
String input = IOUtils.toString(request.getReader());
if (StringUtils.isBlank(input)) {
input = "{}";
}
// isolate input parsing (will include read and write operations)
try (final Tx tx = app.tx()) {
jsonInput = gson.get().fromJson(input, IJsonInput.class);
tx.success();
}
if (securityContext != null) {
// isolate resource authentication
try (final Tx tx = app.tx()) {
resource = ResourceHelper.applyViewTransformation(request, securityContext,
ResourceHelper.optimizeNestedResourceChain(ResourceHelper.parsePath(securityContext, request, resourceMap, propertyView,
config.getDefaultIdProperty()), config.getDefaultIdProperty()), propertyView);
authenticator.checkResourceAccess(request, resource.getResourceSignature(), propertyView.get(securityContext));
tx.success();
}
// isolate doPost
boolean retry = true;
while (retry) {
if (resource.createPostTransaction()) {
try (final Tx tx = app.tx()) {
for (JsonInput propertySet : jsonInput.getJsonInputs()) {
results.add(resource.doPost(convertPropertySetToMap(propertySet)));
}
tx.success();
retry = false;
} catch (DeadlockDetectedException ddex) {
retry = true;
}
} else {
try {
for (JsonInput propertySet : jsonInput.getJsonInputs()) {
results.add(resource.doPost(convertPropertySetToMap(propertySet)));
}
retry = false;
} catch (DeadlockDetectedException ddex) {
retry = true;
}
}
}
// set default value for property view
propertyView.set(securityContext, config.getDefaultPropertyView());
// isolate write output
try (final Tx tx = app.tx()) {
if (!results.isEmpty()) {
final RestMethodResult result = results.get(0);
final int resultCount = results.size();
if (resultCount > 1) {
for (final RestMethodResult r : results) {
final GraphObject objectCreated = r.getContent().get(0);
if (!result.getContent().contains(objectCreated)) {
result.addContent(objectCreated);
}
}
// remove Location header if more than one object was
// written because it may only contain a single URL
result.addHeader("Location", null);
}
result.commitResponse(gson.get(), response);
}
tx.success();
}
} else {
// isolate write output
try (final Tx tx = app.tx()) {
new RestMethodResult(HttpServletResponse.SC_FORBIDDEN).commitResponse(gson.get(), response);
tx.success();
}
}
} catch (FrameworkException frameworkException) {
// set status & write JSON output
response.setStatus(frameworkException.getStatus());
gson.get().toJson(frameworkException, response.getWriter());
response.getWriter().println();
} catch (JsonSyntaxException jsex) {
logger.log(Level.WARNING, "JsonSyntaxException in POST", jsex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in POST: " + jsex.getMessage()));
} catch (JsonParseException jpex) {
logger.log(Level.WARNING, "JsonParseException in POST", jpex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonParseException in POST: " + jpex.getMessage()));
} catch (UnsupportedOperationException uoe) {
logger.log(Level.WARNING, "POST not supported", uoe);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "POST not supported: " + uoe.getMessage()));
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception in POST", t);
int code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in POST: " + t.getMessage()));
} finally {
try {
//response.getWriter().flush();
response.getWriter().close();
} catch (Throwable t) {
logger.log(Level.WARNING, "Unable to flush and close response: {0}", t.getMessage());
}
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="PUT">
@Override
protected void doPut(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
SecurityContext securityContext = null;
Authenticator authenticator = null;
RestMethodResult result = null;
IJsonInput jsonInput = null;
Resource resource = null;
try {
// first thing to do!
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// isolate request authentication in a transaction
try (final Tx tx = StructrApp.getInstance().tx()) {
authenticator = config.getAuthenticator();
securityContext = authenticator.initializeAndExamineRequest(request, response);
tx.success();
}
final App app = StructrApp.getInstance(securityContext);
String input = IOUtils.toString(request.getReader());
if (StringUtils.isBlank(input)) {
input = "{}";
}
// isolate input parsing (will include read and write operations)
try (final Tx tx = app.tx()) {
jsonInput = gson.get().fromJson(input, IJsonInput.class);
tx.success();
}
if (securityContext != null) {
// isolate resource authentication
try (final Tx tx = app.tx()) {
// evaluate constraint chain
resource = ResourceHelper.applyViewTransformation(request, securityContext,
ResourceHelper.optimizeNestedResourceChain(ResourceHelper.parsePath(securityContext, request, resourceMap, propertyView,
config.getDefaultIdProperty()), config.getDefaultIdProperty()), propertyView);
authenticator.checkResourceAccess(request, resource.getResourceSignature(), propertyView.get(securityContext));
tx.success();
}
// isolate doPut
boolean retry = true;
while (retry) {
try (final Tx tx = app.tx()) {
result = resource.doPut(convertPropertySetToMap(jsonInput.getJsonInputs().get(0)));
tx.success();
retry = false;
} catch (DeadlockDetectedException ddex) {
retry = true;
}
}
// isolate write output
try (final Tx tx = app.tx()) {
result.commitResponse(gson.get(), response);
tx.success();
}
} else {
// isolate write output
try (final Tx tx = app.tx()) {
result = new RestMethodResult(HttpServletResponse.SC_FORBIDDEN);
result.commitResponse(gson.get(), response);
tx.success();
}
}
} catch (FrameworkException frameworkException) {
// set status & write JSON output
response.setStatus(frameworkException.getStatus());
gson.get().toJson(frameworkException, response.getWriter());
response.getWriter().println();
} catch (JsonSyntaxException jsex) {
logger.log(Level.WARNING, "JsonSyntaxException in PUT", jsex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in PUT: " + jsex.getMessage()));
} catch (JsonParseException jpex) {
logger.log(Level.WARNING, "JsonParseException in PUT", jpex);
int code = HttpServletResponse.SC_BAD_REQUEST;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in PUT: " + jpex.getMessage()));
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception in PUT", t);
int code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "JsonSyntaxException in PUT: " + t.getMessage()));
} finally {
try {
//response.getWriter().flush();
response.getWriter().close();
} catch (Throwable t) {
logger.log(Level.WARNING, "Unable to flush and close response: {0}", t.getMessage());
}
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="TRACE">
@Override
protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// logRequest("TRACE", request);
response.setContentType("application/json; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
int code = HttpServletResponse.SC_METHOD_NOT_ALLOWED;
response.setStatus(code);
response.getWriter().append(RestMethodResult.jsonError(code, "TRACE method not allowed"));
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="private methods">
private Map<String, Object> convertPropertySetToMap(JsonInput propertySet) {
if (propertySet != null) {
return propertySet.getAttributes();
}
return new LinkedHashMap<>();
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc="nested classes">
private class ThreadLocalPropertyView extends ThreadLocal<String> implements Value<String> {
@Override
protected String initialValue() {
return config.getDefaultPropertyView();
}
@Override
public void set(SecurityContext securityContext, String value) {
set(value);
}
@Override
public String get(SecurityContext securityContext) {
return get();
}
}
private class ThreadLocalGson extends ThreadLocal<Gson> {
private int outputNestingDepth = 3;
public ThreadLocalGson(final int outputNestingDepth) {
this.outputNestingDepth = outputNestingDepth;
}
@Override
protected Gson initialValue() {
JsonInputGSONAdapter jsonInputAdapter = new JsonInputGSONAdapter(propertyView, config.getDefaultIdProperty());
ResultGSONAdapter resultGsonAdapter = new ResultGSONAdapter(propertyView, config.getDefaultIdProperty(), outputNestingDepth);
// create GSON serializer
final GsonBuilder gsonBuilder = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.registerTypeHierarchyAdapter(FrameworkException.class, new FrameworkExceptionGSONAdapter())
.registerTypeAdapter(IJsonInput.class, jsonInputAdapter)
.registerTypeAdapter(Result.class, resultGsonAdapter);
final boolean lenient = Boolean.parseBoolean(StructrApp.getConfigurationValue("json.lenient", "false"));
if (lenient) {
// Serializes NaN, -Infinity, Infinity, see http://code.google.com/p/google-gson/issues/detail?id=378
gsonBuilder.serializeSpecialFloatingPointValues();
}
return gsonBuilder.create();
}
}
// </editor-fold>
}