com.github.brandtg.switchboard.LogRegionResource.java Source code

Java tutorial

Introduction

Here is the source code for com.github.brandtg.switchboard.LogRegionResource.java

Source

/**
 * 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);
            }
        }
    }

}