package com.linkedin.databus2.core.container.request; /* * * Copyright 2013 LinkedIn Corp. All rights reserved * * 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. * */ import java.io.IOException; import java.io.StringWriter; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import org.apache.log4j.Logger; import org.codehaus.jackson.map.ObjectMapper; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import com.linkedin.databus.core.DbusPrettyLogUtils; import com.linkedin.databus.core.data_model.PhysicalPartition; import com.linkedin.databus2.core.container.ChunkedWritableByteChannel; import com.linkedin.databus2.core.container.DatabusHttpHeaders; import com.linkedin.databus2.core.container.netty.ServerContainer; /** * The main class to represent a RESTful request. The request can be associated with a * {@link RequestProcessor} which knows how to interpret the request parameters and how to process * them. Such a request can be called (implements {@link java.util.concurrent.Callable}). The * object also implements {@link java.util.concurrent.Future} so it can be returned directly by * {@link RequestProcessorRegistry#run(DatabusRequest)} if there is no associated * {@link java.util.concurrent.ExecutorService} with the {@link RequestProcessor}. This saves us * a memory allocation for a wrapper object. * */ public class DatabusRequest implements Callable<DatabusRequest>, Future<DatabusRequest> { public static final String MODULE = DatabusRequest.class.getName(); public static final Logger LOG = Logger.getLogger(MODULE); public static final String DATA_PARAM_NAME = "reqdata"; public static final String PATH_PARAM_NAME = "reqpath"; private final static String ERROR_MESSAGE_PREFIX = ""; private final static byte[] FALLBACK_ERROR_MESSAGE_BYTES = "{\"class\":\"unknown\"}".getBytes(Charset.defaultCharset()); private final static String ERROR_MESSAGE_SUFFIX = "\r\n"; private final long _id; private final String _name; private final HttpMethod _requestType; private final Properties _params; private final ServerContainer.RuntimeConfig _config; private final long _createTimestampMs; private final SocketAddress _remoteAddress; /** * _cursorPartition has the last partition from which an event was sent (could be partial or full window) * to the receiver over a channel. For now, this is a context maintained in the server when serving events * from multiple partitions over a single channel (connection). Keeping this context allows the server to * cycle through partitions across responses while not starving out some partitions. Eventually we will move * this object to be sent by the client in the CheckpointMult object as a hint. * * The cursorPartition is only used as a hint, and its absense will not affect the correctness. Specifically, * it is expected that the cursorPartition is ignored if any one of the checkpoints indicates that a partial * window was sent. * * DatabusRequest is merely used as a holder of the server context information since the request processor does * not have access to the netty ChannelContext. */ private PhysicalPartition _cursorPartition = null; private ChunkedWritableByteChannel _responseContent = null; private Throwable _responseThrowable = null; private RequestProcessor _processor = null; //For debugging purposes private static AtomicLong IdCounter = new AtomicLong(1); public DatabusRequest(String name, HttpMethod requestType, SocketAddress remoteAddress, Properties params, ServerContainer.RuntimeConfig config) { super(); _name = name; _params = params; _requestType = requestType; _config = config; _id = IdCounter.getAndIncrement(); _createTimestampMs = System.currentTimeMillis(); _remoteAddress = remoteAddress; } public DatabusRequest(String name, HttpMethod requestType, SocketAddress remoteAddress, ServerContainer.RuntimeConfig config) { this(name, requestType, remoteAddress, new Properties(), config); } public String getName() { return _name; } public Properties getParams() { return _params; } @Override public String toString() { StringBuilder res = new StringBuilder(); res.append("{\"name\":\""); res.append(_name); res.append("\", \"method\":\""); res.append(_requestType); res.append("\" "); for (Object key:_params.keySet()) { Object value = _params.get(key); res.append(", \""); res.append(key.toString()); res.append("\":\""); res.append(null != value ? value.toString() : "null"); res.append("\""); } res.append("}"); return res.toString(); } /** * Obtain the response body to be returned to the user. If {@link DatabusRequest#getResponseThrowable()} * is not null, the body contains the error description. * @return the response body content */ public ChunkedWritableByteChannel getResponseContent() { return _responseContent; } /** * Obtains the error that resulted from the processing of the request if any. * @return the error or null */ public Throwable getResponseThrowable() { return _responseThrowable; } @Override public DatabusRequest call() { if (null != _processor) { try { if (LOG.isDebugEnabled()) { LOG.debug(_name + ": start processing"); } _processor.process(this); if (LOG.isDebugEnabled()) { LOG.debug(_name + ": end processing"); } } catch (Exception e) { DbusPrettyLogUtils.logExceptionAtInfo(getName(), e, LOG); setError(e); } } return this; } public RequestProcessor getProcessor() { return _processor; } public void setProcessor(RequestProcessor processor) { _processor = processor; } /** * Specify that the request processing resulted in an error * @param throwable the throwable describing the error */ public void setError(Throwable throwable) { _responseThrowable = throwable; if (null != _responseThrowable) { //LOG.error("Response error: " + _responseThrowable); _responseContent.addMetadata(DatabusHttpHeaders.DATABUS_ERROR_CLASS_HEADER, _responseThrowable.getClass().getName()); if (_responseThrowable.getMessage() != null) { _responseContent.addMetadata(DatabusHttpHeaders.DATABUS_ERROR_MESSAGE_HEADER, _responseThrowable.getMessage()); } else { _responseContent.addMetadata(DatabusHttpHeaders.DATABUS_ERROR_MESSAGE_HEADER, "No message provided"); } Throwable cause = _responseThrowable.getCause(); if (null != cause) { //LOG.error("Response error caused by: " + cause); _responseContent.addMetadata(DatabusHttpHeaders.DATABUS_ERROR_CAUSE_CLASS_HEADER, cause.getClass().getName()); if (cause.getMessage() != null) { _responseContent.addMetadata(DatabusHttpHeaders.DATABUS_ERROR_CAUSE_MESSAGE_HEADER,cause.getMessage()); } else { _responseContent.addMetadata(DatabusHttpHeaders.DATABUS_ERROR_CAUSE_MESSAGE_HEADER,"No message provided"); } } _responseContent.setResponseCode(HttpResponseStatus.INTERNAL_SERVER_ERROR); HashMap<String, String> exceptionInfo = new HashMap<String, String>(); exceptionInfo.put("error", _responseThrowable.getClass().getName()); exceptionInfo.put("message", _responseThrowable.getMessage()); ObjectMapper mapper = new ObjectMapper(); mapper.getJsonFactory().configure(org.codehaus.jackson.JsonParser.Feature.AUTO_CLOSE_SOURCE, false); StringWriter out = new StringWriter(10240); out.write(ERROR_MESSAGE_PREFIX); byte[] dataBytes; try { mapper.writeValue(out, exceptionInfo); out.write(ERROR_MESSAGE_SUFFIX); out.close(); dataBytes = out.toString().getBytes(Charset.defaultCharset()); } catch (IOException e) { dataBytes = FALLBACK_ERROR_MESSAGE_BYTES; } try { _responseContent.write(ByteBuffer.wrap(dataBytes)); } catch (IOException ioe) { LOG.error("Can't serialize exception " + ioe.toString()); } } } public PhysicalPartition getCursorPartition() { return _cursorPartition; } public void setCursorPartition(PhysicalPartition cursorPartition) { _cursorPartition = cursorPartition; } public HttpMethod getRequestType() { return _requestType; } public ServerContainer.RuntimeConfig getConfig() { return _config; } public int getRequiredIntParam(String paramName) throws InvalidRequestParamValueException { String paramStr = getParams().getProperty(paramName); if (null == paramStr) { throw new InvalidRequestParamValueException(getName(), paramName, "null"); } try { return Integer.parseInt(paramStr); } catch (NumberFormatException nfe) { throw new InvalidRequestParamValueException(getName(), paramName, paramStr); } } public int getOptionalIntParam(String paramName, int defaultValue) throws InvalidRequestParamValueException { String paramStr = getParams().getProperty(paramName); if (null == paramStr) { return defaultValue; } try { return Integer.parseInt(paramStr); } catch (NumberFormatException nfe) { throw new InvalidRequestParamValueException(getName(), paramName, paramStr); } } public long getRequiredLongParam(String paramName) throws InvalidRequestParamValueException { String paramStr = getParams().getProperty(paramName); if (null == paramStr) { throw new InvalidRequestParamValueException(getName(), paramName, "null"); } try { return Long.parseLong(paramStr); } catch (NumberFormatException nfe) { throw new InvalidRequestParamValueException(getName(), paramName, paramStr); } } public long getOptionalLongParam(String paramName, long defaultValue) throws InvalidRequestParamValueException { String paramStr = getParams().getProperty(paramName); if (null == paramStr) { return defaultValue; } try { return Long.parseLong(paramStr); } catch (NumberFormatException nfe) { throw new InvalidRequestParamValueException(getName(), paramName, paramStr); } } public String getRequiredStringParam(String paramName) throws InvalidRequestParamValueException { String paramValue = getParams().getProperty(paramName); if (null == paramValue) { throw new InvalidRequestParamValueException(getName(), paramName, "null"); } return paramValue; } @Override public boolean cancel(boolean arg0) { return false; } @Override public DatabusRequest get() throws InterruptedException, ExecutionException { return this; } @Override public DatabusRequest get(long arg0, TimeUnit arg1) throws InterruptedException, ExecutionException, TimeoutException { return this; } @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return true; } public void setResponseContent(ChunkedWritableByteChannel responseContent) { _responseContent = responseContent; } public long getId() { return _id; } public long getCreateTimestampMs() { return _createTimestampMs; } public SocketAddress getRemoteAddress() { return _remoteAddress; } }