/*
* Copyright © 2015 Cask Data, Inc.
*
* 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 co.cask.cdap.test.app;
import co.cask.cdap.api.Transactional;
import co.cask.cdap.api.TxRunnable;
import co.cask.cdap.api.app.AbstractApplication;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.data.DatasetContext;
import co.cask.cdap.api.dataset.DatasetProperties;
import co.cask.cdap.api.dataset.lib.KeyValueTable;
import co.cask.cdap.api.dataset.table.ConflictDetection;
import co.cask.cdap.api.dataset.table.Table;
import co.cask.cdap.api.service.http.AbstractHttpServiceHandler;
import co.cask.cdap.api.service.http.HttpContentConsumer;
import co.cask.cdap.api.service.http.HttpContentProducer;
import co.cask.cdap.api.service.http.HttpServiceContext;
import co.cask.cdap.api.service.http.HttpServiceRequest;
import co.cask.cdap.api.service.http.HttpServiceResponder;
import co.cask.cdap.common.utils.ImmutablePair;
import com.google.common.base.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
/**
* An application for testing service lifecycle by the {@link ServiceLifeCycleTestRun}.
*/
public class ServiceLifecycleApp extends AbstractApplication {
public static final String HANDLER_TABLE_NAME = "HandlerTable";
@Override
public void configure() {
addService("test", new TestHandler());
createDataset(HANDLER_TABLE_NAME, KeyValueTable.class,
DatasetProperties.builder()
.add(Table.PROPERTY_READLESS_INCREMENT, "true")
.add(Table.PROPERTY_CONFLICT_LEVEL, ConflictDetection.NONE.name())
.build());
}
/**
* Handler for testing that tracks lifecycle method calls through a static list.
*/
public static final class TestHandler extends AbstractHttpServiceHandler {
private static final Queue<ImmutablePair<Integer, String>> STATES = new ConcurrentLinkedQueue<>();
@Override
public void initialize(HttpServiceContext context) throws Exception {
super.initialize(context);
STATES.add(ImmutablePair.of(System.identityHashCode(this), "INIT"));
}
@GET
@Path("/states")
public void getStates(HttpServiceRequest request, HttpServiceResponder responder) {
// Returns the current states
responder.sendJson(new ArrayList<>(STATES));
}
@PUT
@Path("/upload")
public HttpContentConsumer upload(HttpServiceRequest request, HttpServiceResponder responder) {
return new HttpContentConsumer() {
@Override
public void onReceived(ByteBuffer chunk, Transactional transactional) throws Exception {
// No-op
}
@Override
public void onFinish(HttpServiceResponder responder) throws Exception {
responder.sendStatus(200);
}
@Override
public void onError(HttpServiceResponder responder, Throwable failureCause) {
responder.sendString(500, failureCause.getMessage(), Charsets.UTF_8);
}
};
}
@GET
@Path("/download")
public void download(HttpServiceRequest request, HttpServiceResponder responder) {
// Responds with a content producer
KeyValueTable table = getContext().getDataset(HANDLER_TABLE_NAME);
responder.send(200, new DownloadHttpContentProducer(table), "text/plain");
}
@POST
@Path("/uploadDownload")
public HttpContentConsumer uploadDownload(HttpServiceRequest request, HttpServiceResponder responder) {
// Consume request with content consumer, then response with content producer
return new HttpContentConsumer() {
@Override
public void onReceived(ByteBuffer chunk, Transactional transactional) throws Exception {
// no-op
}
@Override
public void onFinish(HttpServiceResponder responder) throws Exception {
KeyValueTable table = getContext().getDataset(HANDLER_TABLE_NAME);
responder.send(200, new DownloadHttpContentProducer(table), "text/plain");
}
@Override
public void onError(HttpServiceResponder responder, Throwable failureCause) {
responder.sendString(500, failureCause.getMessage(), Charsets.UTF_8);
}
};
}
@PUT
@Path("/invalid")
public HttpContentConsumer invalidPut(HttpServiceRequest request, HttpServiceResponder responder) {
// Create a closure on the handler responder
final HttpServiceResponder finalResponder = responder;
return new HttpContentConsumer() {
@Override
public void onReceived(ByteBuffer chunk, Transactional transactional) throws Exception {
// no-op
}
@Override
public void onFinish(HttpServiceResponder responder) throws Exception {
// Use the closure responder instead of the given responder, it should fail
finalResponder.sendStatus(200);
}
@Override
public void onError(HttpServiceResponder responder, Throwable failureCause) {
responder.sendString(500, failureCause.getMessage(), Charsets.UTF_8);
}
};
}
@GET
@Path("/invalid")
public void invalidGet(HttpServiceRequest request, HttpServiceResponder responder,
@QueryParam("methods") final Set<String> methods) {
final Queue<ByteBuffer> chunks = new LinkedList<>();
chunks.add(Charsets.UTF_8.encode("0123456789"));
chunks.add(ByteBuffer.allocate(0));
responder.send(200, new HttpContentProducer() {
@Override
public long getContentLength() {
if (methods.contains("getContentLength")) {
throw new RuntimeException("Exception in getContentLength");
}
return -1L;
}
@Override
public ByteBuffer nextChunk(Transactional transactional) throws Exception {
if (methods.contains("nextChunk")) {
throw new Exception("Exception in nextChunk");
}
return chunks.poll();
}
@Override
public void onFinish() throws Exception {
if (methods.contains("onFinish")) {
throw new Exception("Exception in onFinish");
}
}
@Override
public void onError(Throwable failureCause) {
if (methods.contains("onError")) {
throw new RuntimeException("Exception in onError");
}
}
}, "text/plain");
}
@Override
public void destroy() {
STATES.add(ImmutablePair.of(System.identityHashCode(this), "DESTROY"));
}
/**
* A {@link HttpContentProducer} that will keep producing "0" until it reads a flag from a dataset
*/
private static final class DownloadHttpContentProducer extends HttpContentProducer {
private static final Logger LOG = LoggerFactory.getLogger(DownloadHttpContentProducer.class);
private final KeyValueTable table;
private long sleepMs;
private DownloadHttpContentProducer(KeyValueTable table) {
this.table = table;
}
@Override
public ByteBuffer nextChunk(Transactional transactional) throws Exception {
final AtomicBoolean completed = new AtomicBoolean();
transactional.execute(new TxRunnable() {
@Override
public void run(DatasetContext context) throws Exception {
byte[] value = table.read("completed");
completed.set((!(value == null || value.length != 1)) && Bytes.toBoolean(value));
table.increment(Bytes.toBytes("called"), 1L);
}
});
// Introduce a short delay after sending the first block to avoid sending too much
TimeUnit.MILLISECONDS.sleep(sleepMs);
sleepMs = 100L;
return completed.get() ? ByteBuffer.allocate(0) : Charsets.UTF_8.encode("0");
}
@Override
public void onFinish() throws Exception {
// no-op
}
@Override
public void onError(Throwable failureCause) {
LOG.error("Failure: {}", failureCause.getMessage(), failureCause);
}
}
}
}