/*
* Copyright (C) 2012-2015 DataStax 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 com.datastax.driver.examples.paging;
import com.datastax.driver.core.*;
import com.sun.net.httpserver.HttpServer;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* A stateless REST service (backed by
* <a href="https://jersey.java.net/">Jersey</a>,
* <a href="https://hk2.java.net/">HK2</a> and
* the JDK HttpServer) that displays paginated results for a CQL query.
* <p/>
* Conversion to and from JSON is made through
* <a href="https://jersey.java.net/documentation/latest/media.html#json.jackson">Jersey Jackson providers</a>.
* <p/>
* Navigation is forward-only.
* The implementation relies on the paging state returned by Cassandra, and encodes it in HTTP URLs.
* <p/>
* Preconditions:
* - a Cassandra cluster is running and accessible through the contacts points identified by CONTACT_POINTS and
* CASSANDRA_PORT;
* - port HTTP_PORT is available.
* <p/>
* Side effects:
* - creates a new keyspace "examples" in the cluster. If a keyspace with this name already exists, it will be reused;
* - creates a table "examples.forward_paging_rest_ui". If it already exists, it will be reused;
* - inserts data in the table;
* - launches a REST server listening on HTTP_PORT.
*/
public class ForwardPagingRestUi {
static final String[] CONTACT_POINTS = {"127.0.0.1"};
static final int CASSANDRA_PORT = 9042;
static final int HTTP_PORT = 8080;
static final int ITEMS_PER_PAGE = 10;
static final URI BASE_URI = UriBuilder.fromUri("http://localhost/").path("").port(HTTP_PORT).build();
public static void main(String[] args) throws Exception {
Cluster cluster = null;
try {
cluster = Cluster.builder()
.addContactPoints(CONTACT_POINTS).withPort(CASSANDRA_PORT)
.build();
Session session = cluster.connect();
createSchema(session);
populateSchema(session);
startRestService(session);
} finally {
if (cluster != null) cluster.close();
}
}
// Creates a table storing videos by users, in a typically denormalized way
private static void createSchema(Session session) {
session.execute("CREATE KEYSPACE IF NOT EXISTS examples " +
"WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}");
session.execute("CREATE TABLE IF NOT EXISTS examples.forward_paging_rest_ui(" +
"userid int, username text, " +
"added timestamp, " +
"videoid int, title text, " +
"PRIMARY KEY (userid, added, videoid)" +
") WITH CLUSTERING ORDER BY (added DESC, videoid ASC)");
}
private static void populateSchema(Session session) {
// 3 users
for (int i = 0; i < 3; i++) {
// 49 videos each
for (int j = 0; j < 49; j++) {
int videoid = i * 100 + j;
session.execute("INSERT INTO examples.forward_paging_rest_ui (userid, username, added, videoid, title) VALUES (?, ?, ?, ?, ?)",
i, "user " + i, new Date(j * 100000), videoid, "video " + videoid);
}
}
}
// starts the REST server using JDK HttpServer (com.sun.net.httpserver.HttpServer)
private static void startRestService(Session session) throws IOException, InterruptedException {
final HttpServer server = JdkHttpServerFactory.createHttpServer(BASE_URI, new VideoApplication(session), false);
final ExecutorService executor = Executors.newSingleThreadExecutor();
server.setExecutor(executor);
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println();
System.out.println("Stopping REST Service");
server.stop(0);
executor.shutdownNow();
System.out.println("REST Service stopped");
}
}));
server.start();
System.out.println();
System.out.printf("REST Service started on http://localhost:%d/users, press CTRL+C to stop%n", HTTP_PORT);
System.out.println("To explore this example, start with the following request and walk from there:");
System.out.printf("curl -i http://localhost:%d/users/1/videos%n", HTTP_PORT);
System.out.println();
Thread.currentThread().join();
}
/**
* Configures the REST application and handles injection of custom objects, such
* as the driver session.
* <p>
* This is also the place where you would normally configure JSON serialization, etc.
* <p>
* Note that in this example, we rely on the automatic discovery and configuration of
* Jackson through {@code org.glassfish.jersey.jackson.JacksonFeature}.
*/
public static class VideoApplication extends ResourceConfig {
public VideoApplication(final Session session) {
super(UserService.class);
// AbstractBinder is provided by HK2
register(new AbstractBinder() {
@Override
protected void configure() {
bind(session).to(Session.class);
}
});
}
}
/**
* A typical REST service, handling requests involving users.
* <p/>
* Typically, this service would contain methods for listing and searching for users,
* and methods to retrieve user details. Here, for brevity,
* only one method, listing videos by user, is implemented.
*/
@Singleton
@Path("/users")
@Produces("application/json")
public static class UserService {
@Inject
private Session session;
@Context
private UriInfo uri;
private PreparedStatement videosByUser;
@PostConstruct
@SuppressWarnings("unused")
public void init() {
this.videosByUser = session.prepare("SELECT videoid, title, added FROM examples.forward_paging_rest_ui WHERE userid = ?");
}
/**
* Returns a paginated list of all the videos created by the given user.
*
* @param userid the user ID.
* @param page the page to request, or {@code null} to get the first page.
*/
@GET
@Path("/{userid}/videos")
public UserVideosResponse getUserVideos(@PathParam("userid") int userid, @QueryParam("page") String page) {
Statement statement = videosByUser.bind(userid).setFetchSize(ITEMS_PER_PAGE);
if (page != null)
statement.setPagingState(PagingState.fromString(page));
ResultSet rs = session.execute(statement);
PagingState nextPage = rs.getExecutionInfo().getPagingState();
int remaining = rs.getAvailableWithoutFetching();
List<UserVideo> videos = new ArrayList<UserVideo>(remaining);
if (remaining > 0) {
for (Row row : rs) {
UserVideo video = new UserVideo(
row.getInt("videoid"),
row.getString("title"),
row.getTimestamp("added"));
videos.add(video);
// Make sure we don't go past the current page (we don't want the driver to fetch the next one)
if (--remaining == 0)
break;
}
}
URI next = null;
if (nextPage != null)
next = uri.getAbsolutePathBuilder().queryParam("page", nextPage).build();
return new UserVideosResponse(videos, next);
}
}
public static class UserVideosResponse {
private final List<UserVideo> videos;
private final URI nextPage;
public UserVideosResponse(List<UserVideo> videos, URI nextPage) {
this.videos = videos;
this.nextPage = nextPage;
}
@SuppressWarnings("unused")
public List<UserVideo> getVideos() {
return videos;
}
@SuppressWarnings("unused")
public URI getNextPage() {
return nextPage;
}
}
public static class UserVideo {
private final int videoid;
private final String title;
private final Date added;
public UserVideo(int videoid, String title, Date added) {
this.videoid = videoid;
this.title = title;
this.added = added;
}
@SuppressWarnings("unused")
public int getVideoid() {
return videoid;
}
public String getTitle() {
return title;
}
@SuppressWarnings("unused")
public Date getAdded() {
return added;
}
}
}