io.urmia.api.handler.RestApiHandler.java Source code

Java tutorial

Introduction

Here is the source code for io.urmia.api.handler.RestApiHandler.java

Source

package io.urmia.api.handler;

/**
 *
 * Copyright 2014 by Amin Abbaspour
 *
 * This file is part of Urmia.io
 *
 * Urmia.io is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Urmia.io is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Urmia.io.  If not, see <http://www.gnu.org/licenses/>.
 */

import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.urmia.proxy.HttpProxyFrontendHandler;
import io.urmia.md.model.*;
import io.urmia.md.model.storage.ExtendedObjectAttributes;
import io.urmia.md.model.storage.FullObjectName;
import io.urmia.md.model.storage.FullObjectRequest;
import io.urmia.md.model.storage.ObjectName;
import io.urmia.md.service.MetadataService;
import io.urmia.naming.model.NodeType;
import io.urmia.naming.service.NamingService;
import io.urmia.naming.service.Uuid;
import org.apache.curator.x.discovery.ServiceInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;

import static io.netty.handler.codec.http.HttpHeaders.Names.*;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

public class RestApiHandler extends SimpleChannelInboundHandler<HttpObject> {

    private static final Logger log = LoggerFactory.getLogger(RestApiHandler.class);

    private static FullHttpResponse ERROR_BAD_REQUEST = new DefaultFullHttpResponse(HTTP_1_1,
            HttpResponseStatus.BAD_REQUEST, errorBody("BadRequest", "request is not valid"));

    private static enum RequestState {
        EXPECT_REQUEST, // start
        EXPECT_CONTENT_OR_LAST, // PUT
        EXPECT_LAST // anything else
    }

    private final MetadataService mds;
    private final Uuid uuid;
    private final NamingService ns;

    private volatile RequestState requestState = RequestState.EXPECT_REQUEST;
    private volatile HttpRequest httpRequest = null;
    private volatile ObjectRequest objectRequest = null;
    private volatile boolean proxyMode = false;
    private volatile HttpRequest request;

    public RestApiHandler(MetadataService mds, Uuid uuid, NamingService ns) {
        super(false);
        this.mds = mds;
        this.uuid = uuid;
        this.ns = ns;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("channelActive: {}", ctx);
        super.channelActive(ctx);
        ctx.read();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {

        if (msg instanceof HttpRequest) {

            if (requestState != RequestState.EXPECT_REQUEST) {
                sendError(ctx, ERROR_BAD_REQUEST);
                return;
            }

            request = (HttpRequest) msg;

            log.info("received HttpRequest: {} {}", request.getMethod(), request.getUri());

            final ObjectRequest r;

            try {
                r = new ObjectRequest(request);
            } catch (ExceptionInInitializerError e) {
                log.warn("unable to parse the request into a command: {}", request.getUri());
                return;
            }

            objectRequest = r;
            httpRequest = request;

            if (HttpMethod.PUT.equals(request.getMethod())) {
                if (objectRequest.isNotDirectory()) // make dir request
                    setupProxyToPUT(ctx, objectRequest);

                requestState = RequestState.EXPECT_CONTENT_OR_LAST;
            } else {
                requestState = RequestState.EXPECT_LAST;
                ctx.read();
            }

            return;
        }

        if (objectRequest == null) {
            String uri = request == null ? "" : request.getUri();
            log.warn("not expecting an empty objectRequest. parse error maybe: {}", uri);
            ByteBuf body = request == null || request.getMethod().equals(HttpMethod.HEAD) ? null
                    : errorBody("ResourceNotFoundError", uri + " does not exist");
            sendError(ctx, HttpResponseStatus.NOT_FOUND, body);
            return;
        }

        if (msg instanceof HttpContent) {

            if (requestState != RequestState.EXPECT_LAST && requestState != RequestState.EXPECT_CONTENT_OR_LAST) {
                log.warn("not expecting LAST or CONTENT, requestState: {}", requestState);
                sendError(ctx, HttpResponseStatus.NOT_EXTENDED);
                return;
            }

            final boolean last = msg instanceof LastHttpContent;
            final boolean emptyLast = last && msg == LastHttpContent.EMPTY_LAST_CONTENT;

            if (proxyMode && !emptyLast) // todo: the emptyLast was added for mln
                ctx.fireChannelRead(msg);

            // example of reading only if at the end
            if (last) {

                log.debug("received LastHttpContent: {}", msg);
                requestState = RequestState.EXPECT_REQUEST;

                final HttpRequest request = httpRequest;

                if (HttpMethod.HEAD.equals(request.getMethod())) {
                    handleHEAD(ctx, objectRequest);
                    return;
                }

                if (HttpMethod.GET.equals(request.getMethod())) {
                    handleGET(ctx, objectRequest);
                    return;
                }

                if (HttpMethod.DELETE.equals(request.getMethod())) {
                    handleDELETE(ctx, objectRequest);
                    return;
                }

                if (HttpMethod.PUT.equals(request.getMethod())) {
                    if (proxyMode)
                        log.info("finished file upload: {}", objectRequest);
                    else
                        handlePUTmkdir(ctx, objectRequest);
                    return;
                }

                log.warn("unknown request: {}", request);
                sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            }

            return;
        }

        log.warn("unexpected msg type: {}", msg);
        sendError(ctx, HttpResponseStatus.BAD_REQUEST);
    }

    private void handleGET(final ChannelHandlerContext ctx, final ObjectRequest objectRequest) throws Exception {

        log.info("handleGET req: {}", httpRequest);

        mds.list(ctx.executor(), objectRequest).addListener(new GenericFutureListener<Future<ObjectResponse>>() {
            @Override
            public void operationComplete(Future<ObjectResponse> future) throws Exception {
                if (!future.isSuccess()) {
                    log.error("future failed for handleGET: {}", objectRequest, future.cause());
                    sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
                    return;
                }

                ObjectResponse objectResponse = future.getNow();

                log.info("result of mds.operate: {} -> {}", objectRequest, objectResponse);

                if (objectResponse instanceof ObjectResponse.EmptyResponse) {
                    log.info("nothing found at: {}", objectRequest);
                    sendError(ctx, HttpResponseStatus.NOT_FOUND);
                    return;

                }

                if (objectResponse instanceof ObjectResponse.SingleObject) {
                    log.info("GET is for a file. proxy for downloading: {}", objectResponse);
                    setupProxyToGET(ctx, ((ObjectResponse.SingleObject) objectResponse).fullObjectName);
                    return;

                }

                if (!(objectResponse instanceof ObjectResponse.MultipleObjects)) {
                    log.info("multipleObjects.objects is not MultipleObjects: {}", objectResponse);
                    sendError(ctx, HttpResponseStatus.NOT_FOUND);
                    return;
                }

                log.info("returning result of mds.list: {}", objectResponse.json());
                ctx.writeAndFlush(objectResponse.encode(), ctx.voidPromise());
            }
        });
    }

    private void handleDELETE(final ChannelHandlerContext ctx, final ObjectRequest request) throws IOException {
        log.info("handleDELETE req: {}", httpRequest);

        mds.delete(ctx.executor(), request).addListener(new GenericFutureListener<Future<ObjectResponse>>() {
            @Override
            public void operationComplete(Future<ObjectResponse> future) throws Exception {
                if (future.isSuccess()) {
                    ObjectResponse response = future.get();
                    if (response instanceof ObjectResponse.Failure) {
                        log.warn("error on handleDELETE: {} -> {}", request, response);
                        sendError(ctx, response.encode());
                    } else {
                        log.info("successful handleDELETE req: {}, rsp: {}", request, response);
                        ctx.writeAndFlush(response.encode());
                    }
                } else {
                    log.warn("error on handleDELETE: {}", request, future.cause());
                    sendError(ctx, HttpResponseStatus.BAD_REQUEST);
                }
            }
        });

    }

    private void handleHEAD(final ChannelHandlerContext ctx, final ObjectRequest objectRequest) {

        log.info("handleHEAD uri: {} ", httpRequest.getUri());

        mds.head(ctx.executor(), objectRequest).addListener(new GenericFutureListener<Future<ObjectResponse>>() {
            @Override
            public void operationComplete(Future<ObjectResponse> future) throws Exception {
                if (future.isSuccess()) {
                    ObjectResponse objectResponse = future.get();

                    log.info("handleHEAD {} -> {}", objectRequest, objectResponse);

                    // todo: use .encode()
                    if (objectResponse instanceof ObjectResponse.SingleObject) {
                        ObjectResponse.SingleObject singleObject = (ObjectResponse.SingleObject) objectResponse;

                        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
                                HttpResponseStatus.NO_CONTENT);

                        if (singleObject.fullObjectName.attributes.dir) {
                            response.headers().set(CONTENT_TYPE, "application/x-json-stream; type=directory");
                        } else {
                            response.headers().set(CONTENT_MD5, singleObject.fullObjectName.attributes.md5);
                        }

                        ctx.writeAndFlush(response).addListener(new GenericFutureListener<ChannelFuture>() {

                            @Override
                            public void operationComplete(ChannelFuture future) throws Exception {
                                if (future.isSuccess()) {
                                    log.info("write success");
                                } else {
                                    log.info("write failed: {}", future.cause());
                                }

                            }
                        });

                        return;
                    }

                    log.warn("error on handleHEAD: {}", objectRequest, future.cause());
                    sendError(ctx, ERROR_BAD_REQUEST);
                }
            }
        });

    }

    private void handlePUTmkdir(final ChannelHandlerContext ctx, ObjectRequest request) throws Exception {
        if (request.isNotDirectory()) {
            log.warn("request is not mkdir: {}", request);
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }

        log.info("this is a mkdir: {}", request);
        ExtendedObjectAttributes eoa = new ExtendedObjectAttributes(true, 0, "", uuid.next(), 1,
                System.currentTimeMillis());
        final FullObjectRequest fullObjectRequest = new FullObjectRequest(request, eoa);
        mds.put(ctx.executor(), fullObjectRequest).addListener(new GenericFutureListener<Future<ObjectResponse>>() {
            @Override
            public void operationComplete(Future<ObjectResponse> future) throws Exception {
                if (future.isSuccess()) {
                    ObjectResponse response = future.getNow();
                    log.info("successful mkdir: {}", response);
                    ctx.writeAndFlush(
                            new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT),
                            ctx.voidPromise());
                    ctx.channel().close();
                } else {
                    log.warn("no success on mkdir: {}", future.cause());
                    sendError(ctx, HttpResponseStatus.BAD_REQUEST);
                }
            }
        });
    }

    private void setupProxyToPUT(final ChannelHandlerContext ctx, final ObjectRequest objectRequest)
            throws Exception {

        final List<ServiceInstance<NodeType>> st;
        final Optional<FullObjectName> fon;

        if (objectRequest.isLink()) {
            fon = findByName(objectRequest.getLinkLocation());

            if (fon.isPresent()) {
                st = findAllStorageInstanceUp(mds.storedNodes(fon.get().attributes.etag));
                log.info("LINK proxy storage node: {}, for {} -> {}", st, objectRequest,
                        objectRequest.getLinkLocation());
            } else {
                st = Collections.emptyList();
            }
        } else {
            fon = Optional.absent();

            st = ns.suggestStorage(objectRequest.getDurability());
            log.info("PUT proxy storage node: {}, download mode: {}, durability: {}", st, false,
                    objectRequest.getDurability());
        }

        if (st.isEmpty())
            throw new Exception("not found instances for PUT: " + objectRequest);

        HttpProxyFrontendHandler proxy = new HttpProxyFrontendHandler(st, mds, httpRequest, ctx, false, fon);

        ctx.pipeline().addLast("proxy", proxy);

        proxyMode = true;

        //ctx.read(); // commented to mln

    }

    private Optional<FullObjectName> findByName(String name) {
        Optional<ObjectName> optOn = ObjectName.of(name);
        if (!optOn.isPresent())
            return Optional.absent();
        return mds.selectByName(optOn.get());
    }

    private Optional<ServiceInstance<NodeType>> findStorageInstanceUp(List<String> ids) throws Exception {
        for (String id : ids) {
            ServiceInstance<NodeType> si = ns.discover(NodeType.ODS, id);
            if (si != null)
                return Optional.of(si);
        }
        return Optional.absent();
    }

    private List<ServiceInstance<NodeType>> findAllStorageInstanceUp(List<String> ids) throws Exception {
        List<ServiceInstance<NodeType>> s = new ArrayList<ServiceInstance<NodeType>>(ids.size());
        for (String id : ids) {
            ServiceInstance<NodeType> si = ns.discover(NodeType.ODS, id);
            if (si != null)
                s.add(si);
        }
        return s;
    }

    private void setupProxyToGET(final ChannelHandlerContext ctx, final FullObjectName fon) throws Exception {

        log.info("proxy GET storage node: download mode: {}", true);

        final Future<List<String>> futureStorageNodes = mds.storedNodes(ctx.executor(), fon.attributes.etag);

        futureStorageNodes.addListener(new GenericFutureListener<Future<List<String>>>() {
            @Override
            public void operationComplete(Future<List<String>> future) throws Exception {
                if (future.isSuccess()) {
                    final List<String> st = future.getNow();

                    Optional<ServiceInstance<NodeType>> si = findStorageInstanceUp(st);

                    if (!si.isPresent()) {
                        log.error("failed to find running node, req: {}", objectRequest);
                        sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
                        return;
                    }

                    List<ServiceInstance<NodeType>> l = Lists.newArrayList(si.get());
                    log.info("proxy storage node: {}, download mode: {}, durability: {}", st, true,
                            objectRequest.getDurability());
                    HttpProxyFrontendHandler proxy = new HttpProxyFrontendHandler(l, mds, httpRequest, ctx, true,
                            Optional.<FullObjectName>absent());

                    ctx.pipeline().remove("encoder");

                    ctx.pipeline().addLast("proxy", proxy);

                    proxyMode = true;

                    ctx.read(); // todo: can ve removed?

                } else {
                    log.error("failed to fetch futureStorageNodes, req: {}", objectRequest, future.cause());
                    sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
                }
            }
        });

    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        sendError(ctx, new DefaultFullHttpResponse(HTTP_1_1, status));
    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status, ByteBuf body) {
        final FullHttpResponse response = body == null ? new DefaultFullHttpResponse(HTTP_1_1, status)
                : new DefaultFullHttpResponse(HTTP_1_1, status, body);
        sendError(ctx, response);
    }

    private void sendError(ChannelHandlerContext ctx, FullHttpResponse response) {
        log.info("sending error: {}", response);
        response.headers().set(CONTENT_TYPE, "application/json");
        response.headers().set(CONNECTION, "close");
        ctx.writeAndFlush(response);
        ctx.channel().close();
    }

    private static ByteBuf errorBody(String code, String message) {
        final String json = "{\"code\":\"" + code + "\", \"message\":\"" + message + "\"}";
        return Unpooled.copiedBuffer(json, CharsetUtil.UTF_8);
    }

}