org.dataconservancy.dcs.lineage.http.support.RequestUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.dataconservancy.dcs.lineage.http.support.RequestUtil.java

Source

/*
 * Copyright 2012 Johns Hopkins University
 *
 * 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 org.dataconservancy.dcs.lineage.http.support;

import java.io.IOException;

import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;

import org.dataconservancy.dcs.lineage.api.Lineage;
import org.dataconservancy.dcs.lineage.api.LineageEntry;
import org.dataconservancy.model.dcs.DcsEntity;
import org.joda.time.DateTime;

/**
 * Utility methods for handling various aspects of the Lineage HTTP API request/response.
 */
public class RequestUtil {

    private static final String HOST_HEADER = "Host";

    private static final String LOCAL_HOST_IPV4 = "127.0.0.1";

    private static final String LOCAL_HOST_IPV6 = "0:0:0:0:0:0:0:1%0";

    private static final String LOCAL_HOST_NAME = "localhost";

    private boolean considerHostHeader = true;

    private boolean considerRemotePort = true;

    private boolean considerLocalPort = true;

    private boolean performLocalhostIpTranslation = true;

    public boolean isConsiderHostHeader() {
        return considerHostHeader;
    }

    public void setConsiderHostHeader(boolean considerHostHeader) {
        this.considerHostHeader = considerHostHeader;
    }

    public boolean isConsiderLocalPort() {
        return considerLocalPort;
    }

    public void setConsiderLocalPort(boolean considerLocalPort) {
        this.considerLocalPort = considerLocalPort;
    }

    public boolean isConsiderRemotePort() {
        return considerRemotePort;
    }

    public void setConsiderRemotePort(boolean considerRemotePort) {
        this.considerRemotePort = considerRemotePort;
    }

    public boolean isPerformLocalhostIpTranslation() {
        return performLocalhostIpTranslation;
    }

    public void setPerformLocalhostIpTranslation(boolean performLocalhostIpTranslation) {
        this.performLocalhostIpTranslation = performLocalhostIpTranslation;
    }

    /**
     * Calculates a MD5 digest over the list of entity ids, suitable for use as an
     * ETag.  If the list is empty, {@code null} is returned.
     *
     * @param entityIds the entities
     * @return the MD5 digest, or {@code null} if {@code entityIds} is empty.
     */
    public static String calculateDigest(List<String> entityIds) {
        if (entityIds.isEmpty()) {
            return null;
        }

        NullOutputStream nullOut = new NullOutputStream();
        DigestOutputStream digestOut = null;

        try {
            digestOut = new DigestOutputStream(nullOut, MessageDigest.getInstance("MD5"));
            for (String id : entityIds) {
                IOUtils.write(id, digestOut);
            }
            digestOut.close();
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage(), e);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e.getMessage(), e);
        }

        return digestToHexString(digestOut.getMessageDigest().digest());
    }

    /**
     * Convenience method, simply delegates to {@link #calculateDigest(java.util.List)}.  If the
     * {@code entities} list is empty, {@code null} is returned.
     *
     * @param entities the entities
     * @return the MD5 digest, or {@code null} if the {@code entities List} is empty.
     */
    public static String calculateDigestForEntities(List<DcsEntity> entities) {
        if (entities.isEmpty()) {
            return null;
        }

        List<String> ids = new ArrayList<String>();
        for (DcsEntity e : entities) {
            ids.add(e.getId());
        }

        return calculateDigest(ids);
    }

    /**
     * Convenience method, simply delegates to {@link #calculateDigest(java.util.List)}.  If the
     * lineage is {@code null} or empty, {@code null} is returned.
     *
     * @param l the lineage to calculate a digest for
     * @return the MD5 digest, or {@code null} if the {@code entities List} is empty.
     */
    public static String calculateDigestForLineage(Lineage l) {
        if (l == null || !l.iterator().hasNext()) {
            return null;
        }

        List<String> ids = new ArrayList<String>();
        for (LineageEntry entry : l) {
            ids.add(entry.getEntityId());
        }

        return calculateDigest(ids);
    }

    public DateTime determineLastModified(List<String> entityIds) {
        throw new UnsupportedOperationException("TODO: Implement");
    }

    public String createEtag(String entityId) {
        return calculateDigest(Arrays.asList(entityId));
    }

    public DateTime determineLastModified(String entityId) {
        throw new UnsupportedOperationException("TODO: Implement");
    }

    public boolean determineIfModifiedSince(String id, DateTime modifiedSince) {
        throw new UnsupportedOperationException("TODO: Implement");
    }

    public boolean determineIfModified(String id, String etag) {
        throw new UnsupportedOperationException("TODO: Implement");
    }

    /**
     * Build the original request url in the form http://hostname/request/uri.
     * <p/>
     * If you include a 'Host' HTTP header (which is the form hostname:port), that is what will be used to re-construct
     * the requested URL. If you don't include a 'Host' header, the the behavior is to use
     * HttpServletRequest.getRemoteHost() to determine the hostname, and HttpServletRequest.getRemotePort() to determine
     * the port number.  The logic is influenced by three member variables of RequestUtil: considerHostHeader,
     * considerRemotePort, and considerLocalPort.  The summary above is the default behavior (by default all three flags
     * are true).
     *
     * @param request the request
     * @return the request url
     */
    public String buildRequestUrl(HttpServletRequest request) {
        StringBuilder url = new StringBuilder("http");
        if (request.isSecure()) {
            url.append("s");
        }
        url.append("://");

        url.append(determineHostName(request));

        int port = determinePort(request);

        if (port == 80) {
            if (request.isSecure()) {
                url.append(":80");
            }
        } else if (port == 443) {
            if (!request.isSecure()) {
                url.append(":443");
            }
        } else {
            url.append(":").append(port);
        }

        url.append(request.getRequestURI());

        return url.toString();
    }

    /**
     * Converts a digest represented as a byte array to a hexadecimal string, a la 'md5sum'.
     *
     * @param digest the digest
     * @return the digest in hexadecimal string form
     */
    private static String digestToHexString(byte[] digest) {
        StringBuilder result = new StringBuilder();
        for (byte b : digest) {
            final String hex = Integer.toHexString(b & 0x000000ff);
            if (hex.length() == 1) {
                result.append(0);
            }
            result.append(hex);
        }

        return result.toString();
    }

    /**
     * Determine the host name that the client targeted with their {@code request}.  If {@code considerHostHeader} is
     * {@code true}, and a HTTP {@code Host} header is present, the value of the header will be used as the host name.
     * If the header is not present, or if {@code considerHostHeader} is {@code false}, the host name will be determined
     * using {@link javax.servlet.http.HttpServletRequest#getRemoteHost()}.  If {@code performLocalhostIpTranslation}
     * is {@code true}, and the host name is {@code LOCAL_HOST_IPV4} or {@code LOCAL_HOST_IPV6}, then the host name
     * will be set to {@code LOCAL_HOST_NAME}.
     *
     * @param request the request
     * @return the host name targeted by the {@code request}
     */
    private String determineHostName(HttpServletRequest request) {

        String hostName = null;

        // If there is a 'Host' header with the request, and if
        // we are supposed to consider it when determining the host name,
        // then use it.

        // This is the best way to go, because the client is indicating
        // what host and port they targeted
        final String hostHeader = request.getHeader(HOST_HEADER);
        if (considerHostHeader && hostHeader != null && hostHeader.trim().length() != 0) {
            hostName = parseHostHeader(hostHeader)[0];
        }

        // Either the 'Host' header wasn't considered, or parsing it failed for some reason.
        // So we fall back on request.getRemoteHost()
        if (hostName == null) {
            hostName = request.getRemoteHost();
        }

        if (performLocalhostIpTranslation) {
            if (LOCAL_HOST_IPV4.equals(hostName) || LOCAL_HOST_IPV6.equals(hostName)) {
                hostName = LOCAL_HOST_NAME;
            }
        }

        return hostName;
    }

    /**
     * Determine the port number that the client targeted with their {@code request}.  If {@code considerHostHeader} is
     * {@code true}, and a HTTP {@code Host} header is present, the value of the port in the header will be used as the
     * port number. If the header is not present, or if {@code considerHostHeader} is {@code false}, the port number
     * will be determined using {@link javax.servlet.http.HttpServletRequest#getRemotePort()}
     * (if {@code considerRemotePort} is {@code true}) followed by {@link javax.servlet.http.HttpServletRequest#getLocalPort()}
     * (if {@code considerLocalPort} is {@code true}).
     *
     * @param request the request
     * @return the port number targeted by the {@code request}
     */
    private int determinePort(HttpServletRequest request) {

        int portNumber = -1;

        // If there is a 'Host' header with the request, and if
        // we are supposed to consider it when determining the port,
        // then use it.

        // This is the best way to go, because the client is indicating
        // what host and port they targeted
        final String hostHeader = request.getHeader(HOST_HEADER);
        if (considerHostHeader && hostHeader != null && hostHeader.trim().length() != 0) {
            String temp = parseHostHeader(hostHeader)[1];
            if (temp != null) {
                portNumber = Integer.parseInt(temp);
            }
        }

        // Either the 'Host' header wasn't considered, or parsing it failed for some reason.

        if (portNumber == -1 && considerRemotePort) {
            portNumber = request.getRemotePort();
        }

        if (portNumber == -1 && considerLocalPort) {
            portNumber = request.getLocalPort();
        }

        return portNumber;
    }

    /**
     * Intended to parse the value of the HTTP {@code Host} header.  Typically values will look like
     * {@code hostname:port}.
     *
     * @param hostHeaderValue the value of the {@code Host} HTTP header, must not be {@code null} or empty.
     * @return an array with index 0 containing the host name, and index 1 containing the port
     * @throws IllegalArgumentException if {@code hostHeaderValue} is {@code null} or the empty string
     */
    private String[] parseHostHeader(String hostHeaderValue) {
        // Just to be sure
        if (hostHeaderValue == null || hostHeaderValue.trim().length() == 0) {
            throw new IllegalArgumentException("Host header shouldn't be null or empty.");
        } else {
            hostHeaderValue = hostHeaderValue.trim();
        }

        String[] hostAndPort = hostHeaderValue.split(":");

        // handle the case of a missing port (default to port 80)
        if (hostAndPort.length == 1) {
            hostAndPort = new String[] { hostAndPort[0], "80" };
        }

        return hostAndPort;
    }

}