Java tutorial
/** * Copyright (C) 2015 Greg Brandt (brandt.greg@gmail.com) * * 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.github.brandtg.switchboard; import com.codahale.metrics.annotation.Timed; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.LengthFieldPrepender; import io.netty.handler.stream.ChunkedFile; import io.netty.handler.stream.ChunkedWriteHandler; import org.apache.commons.codec.binary.Base64; import java.io.RandomAccessFile; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import javax.ws.rs.GET; import javax.ws.rs.NotFoundException; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; @Path("/log") @Produces(MediaType.APPLICATION_JSON) public class LogRegionResource { private static final int DEFAULT_COUNT = 10; private final Bootstrap bootstrap; private final LogIndex logIndex; private final LogReader logReader; /** * Creates a resource that can serve log regions. * * @param eventExecutors The Netty executors used to send log regions asynchronously * @param logIndex Used to determine which file regions to send * @param logReader Used to physically read file regions if async is not used */ public LogRegionResource(EventLoopGroup eventExecutors, LogIndex logIndex, LogReader logReader) { this.logIndex = logIndex; this.logReader = logReader; this.bootstrap = new Bootstrap().group(eventExecutors).channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel channel) throws Exception { channel.pipeline().addLast(new LengthFieldPrepender(4)); channel.pipeline().addLast(new ChunkedWriteHandler()); } }); } /** * Returns a list of the logical logs (i.e. collections) the server is aware of. */ @GET public List<String> getCollections() { List<String> collections = new ArrayList<String>(logIndex.getCollections()); Collections.sort(collections); return collections; } /** * Returns the special log metadata header if present. */ @GET @Path("/metadata/header") public LogRegionResponse getHeader(@QueryParam("target") String target) throws Exception { LogRegion header = logIndex.getLogHeader(); if (header == null) { throw new NotFoundException(); } List<LogRegion> logRegions = Collections.singletonList(header); LogRegionResponse response = new LogRegionResponse(); response.setLogRegions(logRegions); handleData(target, logRegions, response); return response; } /** * Returns the latest log region available in the server. * * @param collection * The logical identifier for the log */ @GET @Path("/{collection}/latest") @Timed public LogRegion get(@PathParam("collection") String collection) throws Exception { LogRegion latest = logIndex.getHighWaterMark(collection); if (latest == null) { throw new NotFoundException(); } return latest; } /** * Retrieves log segments and returns data + metadata to user. * * @param collection The logical identifier for the log * @param startIndex The first index that should be queried from the index * @param count The number of regions after the start index to send * @param includeStart If true, include the start index in the set of regions (otherwise omit) * @param target A "host:port" string to which to send data (if null, send data in response) * @throws Exception If there were an error reading the log */ @GET @Path("/{collection}/{startIndex}") @Timed public LogRegionResponse get(@PathParam("collection") String collection, @PathParam("startIndex") Long startIndex, @QueryParam("count") Integer count, @QueryParam("includeStart") boolean includeStart, @QueryParam("target") final String target) throws Exception { if (count == null) { count = DEFAULT_COUNT; } final List<LogRegion> logRegions = logIndex.getLogRegions(collection, startIndex, count, includeStart); if (logRegions.isEmpty()) { throw new NotFoundException(); } LogRegionResponse response = new LogRegionResponse(); response.setLogRegions(logRegions); handleData(target, logRegions, response); return response; } private void handleData(String target, List<LogRegion> logRegions, LogRegionResponse response) throws Exception { if (target != null) { final AtomicInteger contentLength = new AtomicInteger(); for (LogRegion logRegion : logRegions) { contentLength.addAndGet((int) (logRegion.getNextFileOffset() - logRegion.getFileOffset())); } String[] hostPort = target.split(":"); InetSocketAddress socketAddress = new InetSocketAddress(hostPort[0], Integer.valueOf(hostPort[1])); bootstrap.connect(socketAddress).addListener(new LogFileSender(logRegions, target)); response.setDataSize(contentLength.get()); } else { Map<Long, String> data = new HashMap<Long, String>(logRegions.size()); for (LogRegion logRegion : logRegions) { data.put(logRegion.getIndex(), Base64.encodeBase64String(logReader.read(logRegion))); } response.setData(data); } } private class LogFileSender implements ChannelFutureListener { private final Collection<LogRegion> logRegions; private final String target; LogFileSender(Collection<LogRegion> logRegions, String target) { this.logRegions = logRegions; this.target = target; } @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { if (channelFuture.isSuccess()) { for (LogRegion logRegion : logRegions) { channelFuture.channel() .writeAndFlush(new ChunkedFile(new RandomAccessFile(logRegion.getFileName(), "r"), logRegion.getFileOffset(), logRegion.getNextFileOffset() - logRegion.getFileOffset(), 1024)); } } else { throw new IllegalArgumentException("Could not connect to " + target); } } } }