// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.processing.handler; import com.google.inject.Inject; import com.yahoo.component.ComponentId; import com.yahoo.component.ComponentSpecification; import com.yahoo.component.chain.Chain; import com.yahoo.component.chain.ChainedComponent; import com.yahoo.component.chain.model.ChainsModelBuilder; import com.yahoo.component.provider.ComponentRegistry; import com.yahoo.container.core.ChainsConfig; import com.yahoo.container.jdisc.ContentChannelOutputStream; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.container.jdisc.VespaHeaders; import com.yahoo.container.logging.AccessLog; import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.handler.ContentChannel; import com.yahoo.processing.Processor; import com.yahoo.processing.Request; import com.yahoo.processing.Response; import com.yahoo.processing.execution.Execution; import com.yahoo.processing.execution.ResponseReceiver; import com.yahoo.processing.execution.chain.ChainRegistry; import com.yahoo.processing.rendering.AsynchronousSectionedRenderer; import com.yahoo.processing.rendering.ProcessingRenderer; import com.yahoo.processing.rendering.Renderer; import com.yahoo.processing.request.CompoundName; import com.yahoo.processing.request.ErrorMessage; import com.yahoo.processing.request.Properties; import com.yahoo.processing.response.Data; import com.yahoo.processing.response.DataList; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import static com.yahoo.component.chain.ChainsConfigurer.prepareChainRegistry; /** * Superclass of handlers invoking some kind of processing chain. *

* COMPONENT: The type of the processing components of which this executes a chain * * @author bratseth * @author Tony Vaagenes * @author Steinar Knutsen * @since 5.1.6 */ public abstract class AbstractProcessingHandler extends LoggingRequestHandler { private final static CompoundName freezeListenerKey =new CompoundName("processing.freezeListener"); public final static String DEFAULT_RENDERER_ID = "default"; private final Executor renderingExecutor; private ChainRegistry chainRegistry; private final ComponentRegistry renderers; private final Renderer defaultRenderer; public AbstractProcessingHandler(ChainRegistry chainRegistry, ComponentRegistry renderers, Executor executor, AccessLog accessLog, Metric metric) { super(executor, accessLog, metric, true); renderingExecutor = executor; this.chainRegistry = chainRegistry; this.renderers = renderers; // Default is the one with id "default", or the ProcessingRenderer if there is no such renderer Renderer defaultRenderer = renderers.getComponent(ComponentSpecification.fromString(DEFAULT_RENDERER_ID)); if (defaultRenderer == null) { defaultRenderer = new ProcessingRenderer(); renderers.register(ComponentId.fromString(DEFAULT_RENDERER_ID), defaultRenderer); } this.defaultRenderer = defaultRenderer; } public AbstractProcessingHandler(ChainRegistry chainRegistry, ComponentRegistry renderers, Executor executor, AccessLog accessLog) { this(chainRegistry, renderers, executor, accessLog, null); } public AbstractProcessingHandler(ChainsConfig processingChainsConfig, ComponentRegistry chainedComponents, ComponentRegistry renderers, Executor executor, AccessLog accessLog) { this(processingChainsConfig, chainedComponents, renderers, executor, accessLog, null); } @Inject public AbstractProcessingHandler(ChainsConfig processingChainsConfig, ComponentRegistry chainedComponents, ComponentRegistry renderers, Executor executor, AccessLog accessLog, Metric metric) { this(createChainRegistry(processingChainsConfig, chainedComponents), renderers, executor, accessLog, metric); } /** Throws UnsupportedOperationException: Call handle(request, channel instead) */ @Override public HttpResponse handle(HttpRequest request) { throw new UnsupportedOperationException("Call handle(request, channel) instead"); } @Override @SuppressWarnings("unchecked") public HttpResponse handle(HttpRequest request, ContentChannel channel) { com.yahoo.processing.Request processingRequest = new com.yahoo.processing.Request(); populate("", request.propertyMap(), processingRequest.properties()); populate("context", request.getJDiscRequest().context(), processingRequest.properties()); processingRequest.properties().set(Request.JDISC_REQUEST, request); FreezeListener freezeListener = new FreezeListener(processingRequest, renderers, defaultRenderer, channel, renderingExecutor); processingRequest.properties().set(freezeListenerKey, freezeListener); Chain chain = chainRegistry.getComponent(resolveChainId(processingRequest.properties())); if (chain == null) throw new IllegalArgumentException("Chain '" + processingRequest.properties().get("chain") + "' not found"); Execution execution = createExecution(chain, processingRequest); freezeListener.setExecution(execution); Response processingResponse = execution.process(processingRequest); return freezeListener.getHttpResponse(processingResponse); } public Execution createExecution(Chain chain, Request processingRequest) { int traceLevel = processingRequest.properties().getInteger("tracelevel", 0); return Execution.createRoot(chain, traceLevel, new Execution.Environment<>(chainRegistry)); } public ChainRegistry getChainRegistry() { return chainRegistry; } public ComponentRegistry getRenderers() { return renderers; } /** * For internal use only */ @SuppressWarnings("unchecked") public Renderer getRendererCopy(ComponentSpecification spec) { Renderer renderer = getRenderers().getComponent(spec); if (renderer == null) throw new IllegalArgumentException("No renderer with spec: " + spec); return perRenderingCopy(renderer); } private static Renderer perRenderingCopy(Renderer renderer) { Renderer copy = renderer.clone(); copy.init(); return copy; } private static Renderer selectRenderer(com.yahoo.processing.Request processingRequest, ComponentRegistry renderers, Renderer defaultRenderer) { Renderer renderer = null; // TODO: Support setting a particular renderer instead of just selecting // by name? String rendererId = processingRequest.properties().getString("format"); if (rendererId != null && !"".equals(rendererId)) { renderer = renderers.getComponent(ComponentSpecification.fromString(rendererId)); if (renderer == null) processingRequest.errors().add(new ErrorMessage("Could not find renderer","Requested '" + rendererId + "', has " + renderers.allComponents())); } if (renderer == null) renderer = defaultRenderer; return renderer; } private static ChainRegistry createChainRegistry( ChainsConfig processingChainsConfig, ComponentRegistry availableComponents) { ChainRegistry chainRegistry = new ChainRegistry<>(); prepareChainRegistry(chainRegistry, ChainsModelBuilder.buildFromConfig(processingChainsConfig), availableComponents); chainRegistry.freeze(); return chainRegistry; } private String resolveChainId(Properties properties) { return properties.getString(Request.CHAIN,"default"); } private void populate(String prefixName,Map parameters,Properties properties) { CompoundName prefix = new CompoundName(prefixName); for (Map.Entry entry : parameters.entrySet()) properties.set(prefix.append(entry.getKey()),entry.getValue()); } private static class FreezeListener implements Runnable, ResponseReceiver { /** Used to create the renderer */ private final com.yahoo.processing.Request request; private final ComponentRegistry renderers; private final Renderer defaultRenderer; private final ContentChannel channel; private final Executor renderingExecutor; /** Used to render */ private Execution execution; private Response response; /** The renderer used in this, or null if not created yet */ private Renderer renderer = null; public FreezeListener(com.yahoo.processing.Request request, ComponentRegistry renderers, Renderer defaultRenderer, ContentChannel channel, Executor renderingExecutor) { this.request = request; this.renderers = renderers; this.defaultRenderer = defaultRenderer; this.channel = channel; this.renderingExecutor = renderingExecutor; } /** Expected to be called once before run is called */ @Override public void setResponse(Response response) { this.response = response; } /** Expected to be called once before run is called */ public void setExecution(Execution execution) { this.execution = execution; } /** Returns and lazily creates the renderer of this. May be called even if run is never called. */ public Renderer getRenderer() { if (renderer == null) renderer = perRenderingCopy(selectRenderer(request, renderers, defaultRenderer)); return renderer; } /** Returns and lazily creates the http response of this. May be called even if run is never called. */ private HttpResponse getHttpResponse(Response processingResponse) { int status = 200; // true status is determined asynchronously in ProcessingResponse.complete() return new ProcessingResponse(status, request, processingResponse, getRenderer(), renderingExecutor, execution); } @Override public void run() { if (execution == null || response == null) throw new NullPointerException("Uninitialized freeze listener"); if (channel instanceof LazyContentChannel) ((LazyContentChannel)channel).setHttpResponse(getHttpResponse(response)); // Render if we have a renderer capable of it if (getRenderer() instanceof AsynchronousSectionedRenderer) { ((AsynchronousSectionedRenderer) getRenderer()).renderBeforeHandover(new ContentChannelOutputStream(channel), response, execution, request); } } } }