Java tutorial
/* * Copyright 2015 Adaptris Ltd. * * 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.adaptris.core.http.jetty; import static com.adaptris.core.CoreConstants.HTTP_METHOD; import static com.adaptris.core.http.jetty.JettyConstants.JETTY_QUERY_STRING; import static com.adaptris.core.http.jetty.JettyConstants.JETTY_URI; import static com.adaptris.core.http.jetty.JettyConstants.JETTY_URL; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.join; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import com.adaptris.annotation.AdvancedConfig; import com.adaptris.annotation.InputFieldDefault; import com.adaptris.annotation.Removal; import com.adaptris.core.AdaptrisMessage; import com.adaptris.core.AdaptrisMessageConsumerImp; import com.adaptris.core.ClosedState; import com.adaptris.core.CoreException; import com.adaptris.core.WorkflowImp; import com.adaptris.core.WorkflowInterceptor; import com.adaptris.core.http.client.RequestMethodProvider.RequestMethod; import com.adaptris.util.TimeInterval; /** * This is the abstract class for all implementations that make use of Jetty to receive messages. * * * @author lchan * @author $Author: lchan $ */ public abstract class BasicJettyConsumer extends AdaptrisMessageConsumerImp { private static final long DEFAULT_EXPECT_INTERVAL = TimeUnit.SECONDS.toMillis(20); private static final TimeInterval DEFAULT_INTERMEDIATE_WAIT_TIME = new TimeInterval(1L, TimeUnit.SECONDS); private static final String COMMA = ","; private static final List<String> HTTP_METHODS; private static final String EXPECT_102_PROCESSING = "102-Processing"; private static final String HEADER_EXPECT = "Expect"; private transient Servlet jettyServlet; private transient ServletWrapper servletWrapper = null; private transient List<String> acceptedMethods = new ArrayList<>(HTTP_METHODS); @InputFieldDefault(value = "false") @AdvancedConfig private Boolean additionalDebug; @AdvancedConfig @Deprecated @Removal(version = "3.9.0", message = "Use timeoutAction") private TimeInterval maxWaitTime; @AdvancedConfig @InputFieldDefault(value = "return 202 after 10 minutes") private TimeoutAction timeoutAction; @AdvancedConfig @InputFieldDefault(value = "Never") private TimeInterval warnAfter; @AdvancedConfig @InputFieldDefault(value = "20 Seconds") private TimeInterval sendProcessingInterval; @Deprecated @AdvancedConfig @Removal(version = "3.9.0", message = "Use warnAfter") private Long warnAfterMessageHangMillis = null; static { List<String> methods = new ArrayList<>(); for (RequestMethod m : RequestMethod.values()) { methods.add(m.name()); } HTTP_METHODS = Collections.unmodifiableList(methods); } public BasicJettyConsumer() { super(); jettyServlet = new BasicServlet(); changeState(ClosedState.getInstance()); } /** * Create an AdaptrisMessage from the incoming servlet request and response. * * @param request the HttpServletRequest * @param response the HttpServletResponse * @return an AdaptrisMessage instance. * @throws IOException * @throws ServletException * @see HttpServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) */ public abstract AdaptrisMessage createMessage(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException; private boolean submitToWorkflow(AdaptrisMessage msg) { boolean waitForCompletion = false; if (retrieveAdaptrisMessageListener() instanceof WorkflowImp) { List<WorkflowInterceptor> interceptors = ((WorkflowImp) retrieveAdaptrisMessageListener()) .getInterceptors(); for (WorkflowInterceptor i : interceptors) { if (JettyWorkflowInterceptorImpl.class.isAssignableFrom(i.getClass())) { waitForCompletion = true; break; } } } retrieveAdaptrisMessageListener().onAdaptrisMessage(msg); return waitForCompletion; } protected void logHeaders(HttpServletRequest req) { if (additionalDebug()) { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintWriter p = new PrintWriter(out); p.println("Received HTTP Headers"); p.println("URL " + req.getRequestURL()); p.println("URI " + req.getRequestURI()); p.println("Query " + req.getQueryString()); for (Enumeration e = req.getHeaderNames(); e.hasMoreElements();) { String key = (String) e.nextElement(); Enumeration values = req.getHeaders(key); StringBuffer sb = new StringBuffer(); while (values.hasMoreElements()) { sb.append(values.nextElement()).append(","); } String s = sb.toString(); p.println(key + ": " + s.substring(0, s.lastIndexOf(","))); } p.close(); log.trace(out.toString()); } } protected ServletWrapper asServletWrapper() throws CoreException { if (servletWrapper == null) { String destination = ensureIsPath(getDestination().getDestination()); servletWrapper = new ServletWrapper(jettyServlet, destination); } return servletWrapper; } // Ensure that if someone types in http://localhost:8080/fred/blah/blah then // we make sure that we return /fred/blah/blah protected String ensureIsPath(String s) throws CoreException { String result = s; try { URI uri = new URI(s); result = uri.getPath(); } catch (URISyntaxException e) { throw new CoreException(e); } return result; } /** * * @see com.adaptris.core.AdaptrisComponent#init() */ public void init() throws CoreException { if (!isEmpty(getDestination().getFilterExpression())) { acceptedMethods = Arrays.asList(getDestination().getFilterExpression().split(",")); log.trace("Acceptable HTTP methods set to : {}", acceptedMethods); } } /** * * @see com.adaptris.core.AdaptrisComponent#start() */ public void start() throws CoreException { retrieveConnection(JettyServletRegistrar.class).addServlet(asServletWrapper()); } /** * * @see com.adaptris.core.AdaptrisComponent#stop() */ public void stop() { String loggingUrl = ""; try { loggingUrl = asServletWrapper().getUrl(); retrieveConnection(JettyServletRegistrar.class).removeServlet(asServletWrapper()); } catch (CoreException e) { log.warn("Could not remove servlet from jetty engine; Path=[{}]", loggingUrl, e); } } /** * * @see com.adaptris.core.AdaptrisComponent#close() */ public void close() { } /** * @return the maxWaitTime * @deprecated since 3.6.6 use {@link #getTimeoutAction()} instead. */ @Deprecated @Removal(version = "3.9.0", message = "Use #getTimeoutAction()") public TimeInterval getMaxWaitTime() { return maxWaitTime; } /** * Set the max wait time for an individual worker in a workflow to finish. * <p> * This setting only has an impact if the consumer is the entry point for a {@link com.adaptris.core.PoolingWorkflow} instance. In * the event that the wait time is exceeded, then the internal {@link javax.servlet.http.HttpServlet} instance commits the * response in its current state and returns control back to the Jetty engine. * </p> * * @param maxWait the maxWaitTime to set (default 10 minutes) * @deprecated since 3.6.6 use {@link #setTimeoutAction(TimeoutAction)} instead. */ @Deprecated @Removal(version = "3.9.0", message = "Use #setTimeoutAction(TimeoutAction)") public void setMaxWaitTime(TimeInterval maxWait) { maxWaitTime = maxWait; } TimeoutAction timeoutAction() { if (getMaxWaitTime() != null) { log.warn("max-wait-time is deprecated); use timeout-action instead"); return new TimeoutAction(getMaxWaitTime()); } return getTimeoutAction() != null ? getTimeoutAction() : new TimeoutAction(); } public TimeoutAction getTimeoutAction() { return timeoutAction; } /** * Set the behaviour that should occur when the workflow takes too long to finish. * <p> * This setting only has an impact if the consumer is the entry point for a {@link com.adaptris.core.PoolingWorkflow} instance. In * the event that the wait time is exceeded, then the behaviour specified here is done. * </p> * * @param action the action; default is a 202 after 10 minutes. */ public void setTimeoutAction(TimeoutAction action) { this.timeoutAction = action; } public Boolean getAdditionalDebug() { return additionalDebug; } public void setAdditionalDebug(Boolean additionalDebug) { this.additionalDebug = additionalDebug; } boolean additionalDebug() { return BooleanUtils.toBooleanDefaultIfNull(getAdditionalDebug(), false); } /** * @deprecated since 3.5.1 use {@link #getWarnAfter()} instead. */ @Deprecated @Removal(version = "3.9.0", message = "Use #getWarnAfter()") public Long getWarnAfterMessageHangMillis() { return warnAfterMessageHangMillis; } /** * @deprecated since 3.5.1 use {@link #setWarnAfter(TimeInterval)} instead. */ @Deprecated @Removal(version = "3.9.0", message = "Use #setWarnAfter(TimeInterval)") public void setWarnAfterMessageHangMillis(Long w) { this.warnAfterMessageHangMillis = w; } /** * @return the warnAfter */ public TimeInterval getWarnAfter() { return warnAfter; } /** * Log a warning after this interval. * * @param t the warnAfter to set, default is to never warn. */ public void setWarnAfter(TimeInterval t) { this.warnAfter = t; } long warnAfter() { long result = Long.MAX_VALUE; if (getWarnAfterMessageHangMillis() != null) { log.warn("Use of deprecated warn-after-message-hand-millis; use warn-after instead"); result = getWarnAfterMessageHangMillis().longValue(); } else { result = TimeInterval.toMillisecondsDefaultIfNull(getWarnAfter(), Long.MAX_VALUE); } return result; } /** * @return the sendExpectEvery */ public TimeInterval getSendProcessingInterval() { return sendProcessingInterval; } /** * If required send a 102 upon this interval. * * @param t the sendExpectEvery to set, default is 20 seconds. */ public void setSendProcessingInterval(TimeInterval t) { this.sendProcessingInterval = t; } long sendProcessingInterval() { return TimeInterval.toMillisecondsDefaultIfNull(getSendProcessingInterval(), DEFAULT_EXPECT_INTERVAL); } protected class BasicServlet extends HttpServlet { private static final long serialVersionUID = 2007082301L; private transient Map<String, HttpOperation> httpHandlers = null; private transient Timer processingTimer; protected BasicServlet() { } @Override public void destroy() { if (processingTimer != null) { processingTimer.cancel(); processingTimer = null; } super.destroy(); } @Override public void init() throws ServletException { super.init(); processingTimer = new Timer(true); } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { String oldName = renameThread(); try { String method = request.getMethod().toUpperCase(); if (handlers().containsKey(method)) { handlers().get(method).handle(request, response); } else { addAllow(response).sendError(HttpURLConnection.HTTP_BAD_METHOD, "Method Not Allowed"); } } finally { Thread.currentThread().setName(oldName); } } protected Map<String, HttpOperation> handlers() { if (httpHandlers == null) { httpHandlers = new HashMap<String, HttpOperation>(); HttpOperation defaultHandler = new HttpOperation() { public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { processRequest(request, response); } }; for (String m : acceptedMethods) { httpHandlers.put(m.toUpperCase().trim(), defaultHandler); } httpHandlers.put(RequestMethod.OPTIONS.name(), new HttpOperation() { public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { addAllow(response).setStatus(HttpURLConnection.HTTP_OK); } }); } return httpHandlers; } private void processRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { AdaptrisMessage msg = createMessage(request, response); ProcessingTimerTask task = null; // If we have a Expect: 102-Processing head, then let's fork a little timer thread to write one // if (hasExpect102(request)) { task = schedule(new ProcessingTimerTask(response)); } msg.addMetadata(JETTY_URL, request.getRequestURL().toString()); msg.addMetadata(JETTY_URI, request.getRequestURI()); msg.addMetadata(HTTP_METHOD, request.getMethod()); if (!isEmpty(request.getQueryString())) { msg.addMetadata(JETTY_QUERY_STRING, request.getQueryString()); } JettyWrapper wrapper = new JettyWrapper().withMonitor(new JettyConsumerMonitor()).withRequest(request) .withResponse(response); msg.addObjectHeader(JettyConstants.JETTY_WRAPPER, wrapper); waitFor(submitToWorkflow(msg), wrapper.getMonitor(), response, msg.getUniqueId()); cancel(task); } private void waitFor(boolean waitFor, JettyConsumerMonitor monitor, HttpServletResponse response, String loggingId) throws IOException, ServletException { if (waitFor) { TimeoutAction timeout = timeoutAction(); try { synchronized (monitor) { while (!monitor.isMessageComplete()) { timeout.checkTimeout(monitor); monitor.wait(DEFAULT_INTERMEDIATE_WAIT_TIME.toMilliseconds()); } } } catch (InterruptedException e) { } catch (TimeoutException e) { timeout.handleTimeout(response); } if ((monitor.getEndTime() - monitor.getStartTime()) > warnAfter()) { log.warn("Message ({}) took longer than expected; {}ms", loggingId, ((monitor.getEndTime() - monitor.getStartTime()))); } } } private void cancel(TimerTask task) { if (task != null) { task.cancel(); } } private ProcessingTimerTask schedule(ProcessingTimerTask task) { // Every 20 seconds as per RFC2518 long interval = sendProcessingInterval(); log.trace("Scheduling a 102 Processing Response every {}ms", interval); processingTimer.schedule(task, interval, interval); return task; } private boolean hasExpect102(HttpServletRequest request) { String expectHeader = null; for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements();) { String hdr = e.nextElement(); if (StringUtils.equalsIgnoreCase(HEADER_EXPECT, hdr)) { expectHeader = request.getHeader(hdr); break; } } return StringUtils.containsIgnoreCase(expectHeader, EXPECT_102_PROCESSING); } private HttpServletResponse addAllow(HttpServletResponse response) throws IOException, ServletException { response.addHeader("Allow", join(handlers().keySet(), COMMA)); return response; } } public interface HttpOperation { void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException; } class ProcessingTimerTask extends TimerTask { private transient HttpServletResponse myResponse; public ProcessingTimerTask(HttpServletResponse response) { myResponse = response; } @Override public void run() { try { if (!myResponse.isCommitted()) { myResponse.sendError(102); } } catch (Exception e) { // In the event of an exception, cancel ourselves. this.cancel(); } } } }