/* * Copyright © 2014-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.client; import co.cask.cdap.api.annotation.Beta; import co.cask.cdap.api.data.schema.Schema; import co.cask.cdap.api.flow.flowlet.StreamEvent; import co.cask.cdap.client.config.ClientConfig; import co.cask.cdap.client.util.RESTClient; import co.cask.cdap.common.BadRequestException; import co.cask.cdap.common.StreamNotFoundException; import co.cask.cdap.common.UnauthenticatedException; import co.cask.cdap.common.stream.StreamEventTypeAdapter; import co.cask.cdap.common.utils.TimeMathParser; import co.cask.cdap.internal.io.SchemaTypeAdapter; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.StreamDetail; import co.cask.cdap.proto.StreamProperties; import co.cask.cdap.proto.StreamRecord; import co.cask.cdap.security.authentication.client.AccessToken; import co.cask.common.http.HttpMethod; import co.cask.common.http.HttpRequest; import co.cask.common.http.HttpRequests; import co.cask.common.http.HttpResponse; import co.cask.common.http.ObjectResponse; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import com.google.common.io.InputSupplier; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.inject.Inject; import javax.net.ssl.HttpsURLConnection; import javax.ws.rs.core.HttpHeaders; /** * Provides ways to interact with CDAP Streams. */ @Beta public class StreamClient { private static final Gson GSON = StreamEventTypeAdapter.register( new GsonBuilder().registerTypeAdapter(Schema.class, new SchemaTypeAdapter())).create(); private final RESTClient restClient; private final ClientConfig config; @Inject public StreamClient(ClientConfig config, RESTClient restClient) { this.config = config; this.restClient = restClient; } public StreamClient(ClientConfig config) { this(config, new RESTClient(config)); } /** * Gets the configuration of a stream. * * @param stream ID of the stream * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream was not found */ public StreamProperties getConfig(Id.Stream stream) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s", stream.getId())); HttpResponse response = restClient.execute(HttpMethod.GET, url, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } return GSON.fromJson(response.getResponseBodyAsString(Charsets.UTF_8), StreamProperties.class); } /** * Sets properties of a stream. * * @param stream ID of the stream * @param properties properties to set * @throws IOException if a network error occurred * @throws UnauthenticatedException if the client is unauthorized * @throws BadRequestException if the request is bad * @throws StreamNotFoundException if the stream was not found */ public void setStreamProperties(Id.Stream stream, StreamProperties properties) throws IOException, UnauthenticatedException, BadRequestException, StreamNotFoundException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s/properties", stream.getId())); HttpRequest request = HttpRequest.put(url).withBody(GSON.toJson(properties)).build(); HttpResponse response = restClient.execute(request, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND, HttpURLConnection.HTTP_BAD_REQUEST); if (response.getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) { throw new BadRequestException("Bad request: " + response.getResponseBodyAsString()); } if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } } /** * Creates a stream. * * @param newStreamId ID of the new stream to create * @throws IOException if a network error occurred * @throws BadRequestException if the provided stream ID was invalid */ public void create(Id.Stream newStreamId) throws IOException, BadRequestException, UnauthenticatedException { create(newStreamId, null); } /** * Creates a stream with {@link StreamProperties} properties. * * @param newStreamId ID of the new stream to create * @param properties {@link StreamProperties} for the new stream * @throws IOException if a network error occurred * @throws BadRequestException if the provided stream ID was invalid */ public void create(Id.Stream newStreamId, @Nullable StreamProperties properties) throws IOException, BadRequestException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(newStreamId.getNamespace(), String.format("streams/%s", newStreamId.getId())); HttpRequest.Builder builder = HttpRequest.put(url); if (properties != null) { builder = builder.withBody(GSON.toJson(properties)); } HttpResponse response = restClient.execute(builder.build(), config.getAccessToken(), HttpURLConnection.HTTP_BAD_REQUEST); if (response.getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) { throw new BadRequestException("Bad request: " + response.getResponseBodyAsString()); } } /** * Sets the description of a stream. * * @param stream ID of the stream * @param description description of the stream * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified ID was not found */ public void setDescription(Id.Stream stream, String description) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s/properties", stream.getId())); HttpRequest request = HttpRequest.put(url).withBody(GSON.toJson( ImmutableMap.of("description", description))).build(); HttpResponse response = restClient.execute(request, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } } /** * Sends an event to a stream. * * @param stream ID of the stream * @param event event to send to the stream * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified ID was not found */ public void sendEvent(Id.Stream stream, String event) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s", stream.getId())); writeEvent(url, stream, event); } /** * Sends an event to a stream. The write is asynchronous, meaning when this method returns, it only guarantees * the event has been received by the server, but may not get persisted. * * @param stream ID of the stream * @param event event to send to the stream * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified ID was not found */ public void asyncSendEvent(Id.Stream stream, String event) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s/async", stream.getId())); writeEvent(url, stream, event); } /** * Sends a file of the given content type to a stream batch endpoint. * * @param stream ID of the stream * @param contentType content type of the file * @param file the file to upload * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified ID was not found */ public void sendFile(Id.Stream stream, String contentType, File file) throws IOException, StreamNotFoundException, UnauthenticatedException { sendBatch(stream, contentType, Files.newInputStreamSupplier(file)); } /** * Sends a batch request to a stream batch endpoint. * * @param stream ID of the stream * @param contentType content type of the data * @param inputSupplier provides content for the batch request * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified ID was not found */ public void sendBatch(Id.Stream stream, String contentType, InputSupplier<? extends InputStream> inputSupplier) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s/batch", stream.getId())); Map<String, String> headers = ImmutableMap.of("Content-type", contentType); HttpRequest request = HttpRequest.post(url).addHeaders(headers).withBody(inputSupplier).build(); HttpResponse response = restClient.upload(request, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } } /** * Truncates a stream, deleting all stream events belonging to the stream. * * @param stream ID of the stream to truncate * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified name was not found */ public void truncate(Id.Stream stream) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s/truncate", stream.getId())); HttpResponse response = restClient.execute(HttpMethod.POST, url, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } } /** * Deletes a stream. * * @param stream ID of the stream to truncate * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified name was not found */ public void delete(Id.Stream stream) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s", stream.getId())); HttpResponse response = restClient.execute(HttpMethod.DELETE, url, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } } /** * Sets the Time-to-Live (TTL) of a stream. TTL governs how long stream events are readable. * * @param stream ID of the stream * @param ttlInSeconds desired TTL, in seconds * @throws IOException if a network error occurred * @throws StreamNotFoundException if the stream with the specified name was not found */ public void setTTL(Id.Stream stream, long ttlInSeconds) throws IOException, StreamNotFoundException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(stream.getNamespace(), String.format("streams/%s/properties", stream.getId())); HttpRequest request = HttpRequest.put(url).withBody(GSON.toJson(ImmutableMap.of("ttl", ttlInSeconds))).build(); HttpResponse response = restClient.execute(request, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } } /** * Lists all streams. * * @return list of {@link StreamRecord}s * @throws IOException if a network error occurred */ public List<StreamDetail> list(Id.Namespace namespace) throws IOException, UnauthenticatedException { URL url = config.resolveNamespacedURLV3(namespace, "streams"); HttpResponse response = restClient.execute(HttpMethod.GET, url, config.getAccessToken()); return ObjectResponse.fromJsonBody(response, new TypeToken<List<StreamDetail>>() { }).getResponseObject(); } /** * Reads events from a stream * * @param streamId ID of the stream * @param startTime Timestamp in milliseconds to start reading event from (inclusive) * @param endTime Timestamp in milliseconds for the last event to read (exclusive) * @param limit Maximum number of events to read * @param results Collection for storing the resulting stream events * @param <T> Type of the collection for storing results * @return The same collection object as passed in the {@code results} parameter * @throws IOException If fails to read from stream * @throws StreamNotFoundException If the given stream does not exists */ public <T extends Collection<? super StreamEvent>> T getEvents(Id.Stream streamId, String startTime, String endTime, int limit, final T results) throws IOException, StreamNotFoundException, UnauthenticatedException { getEvents(streamId, startTime, endTime, limit, new Function<StreamEvent, Boolean>() { @Override public Boolean apply(StreamEvent input) { results.add(input); return true; } }); return results; } /** * Reads events from a stream * * @param streamId ID of the stream * @param startTime Timestamp in milliseconds to start reading event from (inclusive) * @param endTime Timestamp in milliseconds for the last event to read (exclusive) * @param limit Maximum number of events to read * @param results Collection for storing the resulting stream events * @param <T> Type of the collection for storing results * @return The same collection object as passed in the {@code results} parameter * @throws IOException If fails to read from stream * @throws StreamNotFoundException If the given stream does not exists */ public <T extends Collection<? super StreamEvent>> T getEvents(Id.Stream streamId, long startTime, long endTime, int limit, final T results) throws IOException, StreamNotFoundException, UnauthenticatedException { return getEvents(streamId, String.valueOf(startTime), String.valueOf(endTime), limit, results); } /** * Reads events from a stream * * @param streamId ID of the stream * @param startTime Timestamp in milliseconds to start reading event from (inclusive) * @param endTime Timestamp in milliseconds for the last event to read (exclusive) * @param limit Maximum number of events to read * @param callback Callback to invoke for each stream event read. If the callback function returns {@code false} * upon invocation, it will stops the reading * @throws IOException If fails to read from stream * @throws StreamNotFoundException If the given stream does not exists */ public void getEvents(Id.Stream streamId, long startTime, long endTime, int limit, Function<? super StreamEvent, Boolean> callback) throws IOException, StreamNotFoundException, UnauthenticatedException { getEvents(streamId, String.valueOf(startTime), String.valueOf(endTime), limit, callback); } /** * Reads events from a stream * * @param streamId ID of the stream * @param start Timestamp in milliseconds or now-xs format to start reading event from (inclusive) * @param end Timestamp in milliseconds or now-xs format for the last event to read (exclusive) * @param limit Maximum number of events to read * @param callback Callback to invoke for each stream event read. If the callback function returns {@code false} * upon invocation, it will stops the reading * @throws IOException If fails to read from stream * @throws StreamNotFoundException If the given stream does not exists */ public void getEvents(Id.Stream streamId, String start, String end, int limit, Function<? super StreamEvent, Boolean> callback) throws IOException, StreamNotFoundException, UnauthenticatedException { long startTime = TimeMathParser.parseTime(start, TimeUnit.MILLISECONDS); long endTime = TimeMathParser.parseTime(end, TimeUnit.MILLISECONDS); URL url = config.resolveNamespacedURLV3(streamId.getNamespace(), String.format("streams/%s/events?start=%d&end=%d&limit=%d", streamId.getId(), startTime, endTime, limit)); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); AccessToken accessToken = config.getAccessToken(); if (accessToken != null) { urlConn.setRequestProperty(HttpHeaders.AUTHORIZATION, accessToken.getTokenType() + " " + accessToken.getValue()); } if (urlConn instanceof HttpsURLConnection && !config.isVerifySSLCert()) { try { HttpRequests.disableCertCheck((HttpsURLConnection) urlConn); } catch (Exception e) { // TODO: Log "Got exception while disabling SSL certificate check for request.getURL()" } } try { if (urlConn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { throw new UnauthenticatedException("Unauthorized status code received from the server."); } if (urlConn.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(streamId); } if (urlConn.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) { return; } // The response is an array of stream event object InputStream inputStream = urlConn.getInputStream(); JsonReader jsonReader = new JsonReader(new InputStreamReader(inputStream, Charsets.UTF_8)); jsonReader.beginArray(); while (jsonReader.peek() != JsonToken.END_ARRAY) { Boolean result = callback.apply(GSON.<StreamEvent>fromJson(jsonReader, StreamEvent.class)); if (result == null || !result) { break; } } drain(inputStream); // No need to close reader, the urlConn.disconnect in finally will close all underlying streams } finally { urlConn.disconnect(); } } /** * Writes stream event using the given URL. The write maybe sync or async, depending on the URL. */ private void writeEvent(URL url, Id.Stream stream, String event) throws IOException, StreamNotFoundException, UnauthenticatedException { HttpRequest request = HttpRequest.post(url).withBody(event).build(); HttpResponse response = restClient.execute(request, config.getAccessToken(), HttpURLConnection.HTTP_NOT_FOUND); if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { throw new StreamNotFoundException(stream); } } @SuppressWarnings("StatementWithEmptyBody") private void drain(InputStream input) throws IOException { while (input.skip(Long.MAX_VALUE) > 0) { // empty } } }