Package ch.ethz.inf.vs.californium.resources.proxy

Source Code of ch.ethz.inf.vs.californium.resources.proxy.HttpTranslator

package ch.ethz.inf.vs.californium.resources.proxy;

/*******************************************************************************
* Copyright (c) 2012, Institute for Pervasive Computing, ETH Zurich.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* This file is part of the Californium (Cf) CoAP framework.
******************************************************************************/


import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.UnmappableCharacterException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.logging.Logger;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.RequestLine;
import org.apache.http.StatusLine;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.EnglishReasonPhraseCatalog;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicRequestLine;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.util.EntityUtils;

import ch.ethz.inf.vs.californium.coap.CoAP.Code;
import ch.ethz.inf.vs.californium.coap.CoAP.OptionRegistry;
import ch.ethz.inf.vs.californium.coap.CoAP.ResponseCode;
import ch.ethz.inf.vs.californium.coap.MediaTypeRegistry;
import ch.ethz.inf.vs.californium.coap.Message;
import ch.ethz.inf.vs.californium.coap.Option;
import ch.ethz.inf.vs.californium.coap.OptionNumberRegistry;
import ch.ethz.inf.vs.californium.coap.OptionNumberRegistry.optionFormats;
import ch.ethz.inf.vs.californium.coap.Request;
import ch.ethz.inf.vs.californium.coap.Response;
import ch.ethz.inf.vs.californium.proxy.MappingProperties;

/**
* Class providing the translations (mappings) from the HTTP message
* representations to the CoAP message representations and vice versa.
*
* @author Francesco Corazza
* @version $Revision: 1.0 $
*/
public final class HttpTranslator {

  private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
  private static final Charset UTF_8 = Charset.forName("UTF-8");
  private static final String KEY_COAP_CODE = "coap.response.code.";
  private static final String KEY_COAP_OPTION = "coap.message.option.";
  private static final String KEY_COAP_MEDIA = "coap.message.media.";
  private static final String KEY_HTTP_CODE = "http.response.code.";
  private static final String KEY_HTTP_METHOD = "http.request.method.";
  private static final String KEY_HTTP_HEADER = "http.message.header.";
  private static final String KEY_HTTP_CONTENT_TYPE = "http.message.content-type.";

  /**
   * Property file containing the mappings between coap messages and http
   * messages.
   */
  public static final Properties HTTP_TRANSLATION_PROPERTIES = new MappingProperties("Proxy.properties");

  // Error constants
  public static final int STATUS_TIMEOUT = HttpStatus.SC_GATEWAY_TIMEOUT;
  public static final int STATUS_NOT_FOUND = HttpStatus.SC_BAD_GATEWAY;
  public static final int STATUS_TRANSLATION_ERROR = HttpStatus.SC_BAD_GATEWAY;
  public static final int STATUS_URI_MALFORMED = HttpStatus.SC_BAD_REQUEST;
  public static final int STATUS_WRONG_METHOD = HttpStatus.SC_NOT_IMPLEMENTED;

  protected static final Logger LOGGER = Logger.getLogger(HttpTranslator.class.getName());

  /**
   * Gets the coap media type associated to the http entity. Firstly, it looks
   * for a valid mapping in the property file. If this step fails, then it
   * tries to explicitly map/parse the declared mime/type by the http entity.
   * If even this step fails, it sets application/octet-stream as
   * content-type.
   *
   * @param httpMessage
   *
   *
   * @return the coap media code associated to the http message entity. * @see
   *         HttpHeader, ContentType, MediaTypeRegistry
   */
  public static int getCoapMediaType(HttpMessage httpMessage) {
    if (httpMessage == null) {
      throw new IllegalArgumentException("httpMessage == null");
    }

    // get the entity
    HttpEntity httpEntity = null;
    if (httpMessage instanceof HttpResponse) {
      httpEntity = ((HttpResponse) httpMessage).getEntity();
    } else if (httpMessage instanceof HttpEntityEnclosingRequest) {
      httpEntity = ((HttpEntityEnclosingRequest) httpMessage).getEntity();
    }

    // check that the entity is actually present in the http message
    if (httpEntity == null) {
      throw new IllegalArgumentException("The http message does not contain any httpEntity.");
    }

    // set the content-type with a default value
    int coapContentType = MediaTypeRegistry.UNDEFINED;

    // get the content-type from the entity
    ContentType contentType = ContentType.get(httpEntity);
    if (contentType == null) {
      // if the content-type is not set, search in the headers
      Header contentTypeHeader = httpMessage.getFirstHeader("content-type");
      if (contentTypeHeader != null) {
        String contentTypeString = contentTypeHeader.getValue();
        contentType = ContentType.parse(contentTypeString);
      }
    }

    // check if there is an associated content-type with the current http
    // message
    if (contentType != null) {
      // get the value of the content-type
      String httpContentTypeString = contentType.getMimeType();
      // delete the last part (if any)
      httpContentTypeString = httpContentTypeString.split(";")[0];

      // retrieve the mapping from the property file
      String coapContentTypeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_HTTP_CONTENT_TYPE + httpContentTypeString);

      if (coapContentTypeString != null) {
        coapContentType = Integer.parseInt(coapContentTypeString);
      } else {
        // try to parse the media type if the property file has given to
        // mapping
        coapContentType = MediaTypeRegistry.parse(httpContentTypeString);
      }
    }

    // if not recognized, the content-type should be
    // application/octet-stream (draft-castellani-core-http-mapping 6.2)
    if (coapContentType == MediaTypeRegistry.UNDEFINED) {
      coapContentType = MediaTypeRegistry.APPLICATION_OCTET_STREAM;
    }

    return coapContentType;
  }

  /**
   * Gets the coap options starting from an array of http headers. The
   * content-type is not handled by this method. The method iterates over an
   * array of headers and for each of them tries to find a mapping in the
   * properties file, if the mapping does not exists it skips the header
   * ignoring it. The method handles separately certain headers which are
   * translated to options (such as accept or cache-control) whose content
   * should be semantically checked or requires ad-hoc translation. Otherwise,
   * the headers content is translated with the appropriate format required by
   * the mapped option.
   *
   * @param headers
   *
   * @return List<Option>
   */
  public static List<Option> getCoapOptions(Header[] headers) {
    if (headers == null) {
      throw new IllegalArgumentException("httpMessage == null");
    }

    List<Option> optionList = new LinkedList<Option>();

    // iterate over the headers
    for (Header header : headers) {
      try {
        String headerName = header.getName().toLowerCase();
       
        // FIXME: CoAP does no longer support multiple accept-options.
        // If an HTTP request contains multiple accepts, this method
        // fails. Therefore, we currently skip accepts at the moment.
        if (headerName.startsWith("accept"))
            continue;
 
        // get the mapping from the property file
        String optionCodeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_HTTP_HEADER + headerName);
 
        // ignore the header if not found in the properties file
        if (optionCodeString == null || optionCodeString.isEmpty()) {
          continue;
        }
 
        // get the option number
        int optionNumber = OptionRegistry.RESERVED_0;
        try {
          optionNumber = Integer.parseInt(optionCodeString.trim());
        } catch (Exception e) {
          LOGGER.warning("Problems in the parsing: " + e.getMessage());
          // ignore the option if not recognized
          continue;
        }
 
        // ignore the content-type because it will be handled within the
        // payload
        if (optionNumber == OptionRegistry.CONTENT_FORMAT) {
          continue;
        }
 
        // get the value of the current header
        String headerValue = header.getValue().trim();
 
        // if the option is accept, it needs to translate the
        // values
        if (optionNumber == OptionRegistry.ACCEPT) {
          // remove the part where the client express the weight of each
          // choice
          headerValue = headerValue.trim().split(";")[0].trim();
 
          // iterate for each content-type indicated
          for (String headerFragment : headerValue.split(",")) {
            // translate the content-type
            Integer[] coapContentTypes = { MediaTypeRegistry.UNDEFINED };
            if (headerFragment.contains("*")) {
              coapContentTypes = MediaTypeRegistry.parseWildcard(headerFragment);
            } else {
              coapContentTypes[0] = MediaTypeRegistry.parse(headerFragment);
            }
 
            // if is present a conversion for the content-type, then add
            // a new option
            for (int coapContentType : coapContentTypes) {
              if (coapContentType != MediaTypeRegistry.UNDEFINED) {
                // create the option
                Option option = new Option(optionNumber, coapContentType);
                optionList.add(option);
              }
            }
          }
        } else if (optionNumber == OptionRegistry.MAX_AGE) {
          int maxAge = 0;
          if (!headerValue.contains("no-cache")) {
            headerValue = headerValue.split(",")[0];
            if (headerValue != null) {
              int index = headerValue.indexOf('=');
              try {
                maxAge = Integer.parseInt(headerValue.substring(index + 1).trim());
              } catch (NumberFormatException e) {
                LOGGER.warning("Cannot convert cache control in max-age option");
                continue;
              }
            }
          }
          // create the option
          Option option = new Option(optionNumber, maxAge);
          // option.setValue(headerValue.getBytes(Charset.forName("ISO-8859-1")));
          optionList.add(option);
        } else {
          // create the option
          Option option = new Option(optionNumber);
          switch (OptionNumberRegistry.getFormatByNr(optionNumber)) {
          case INTEGER:
            option.setIntegerValue(Integer.parseInt(headerValue));
            break;
          case OPAQUE:
            option.setValue(headerValue.getBytes(ISO_8859_1));
            break;
          case STRING:
          default:
            option.setStringValue(headerValue);
            break;
          }
          // option.setValue(headerValue.getBytes(Charset.forName("ISO-8859-1")));
          optionList.add(option);
        }
      } catch (RuntimeException e) {
        // Martin: I have added this try-catch block. The problem is
        // that HTTP support multiple Accepts while CoAP does not. A
        // headder line might look like this:
        // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
        // This cannot be parsed into a single CoAP Option and yields a
        // NumberFormatException
        LOGGER.warning("Could not parse header line "+header);
      }
    } // while (headerIterator.hasNext())

    return optionList;
  }

  /**
   * Method to map the http entity of a http message in a coherent payload for
   * the coap message. The method simply gets the bytes from the entity and,
   * if needed changes the charset of the obtained bytes to UTF-8.
   *
   * @param httpEntity
   *            the http entity
   *
   * @return byte[]
   * @throws TranslationException
   *             the translation exception
   */
  public static byte[] getCoapPayload(HttpEntity httpEntity) throws TranslationException {
    if (httpEntity == null) {
      throw new IllegalArgumentException("httpEntity == null");
    }

    byte[] payload = null;
    try {
      // get the bytes from the entity
      payload = EntityUtils.toByteArray(httpEntity);
      if (payload != null && payload.length > 0) {

        // the only supported charset in CoAP is UTF-8
        Charset coapCharset = UTF_8;

        // get the charset for the http entity
        ContentType httpContentType = ContentType.getOrDefault(httpEntity);
        Charset httpCharset = httpContentType.getCharset();

        // check if the charset is the one allowed by coap
        if (httpCharset != null && !httpCharset.equals(coapCharset)) {
          // translate the payload to the utf-8 charset
          payload = changeCharset(payload, httpCharset, coapCharset);
        }
      }
    } catch (IOException e) {
      LOGGER.warning("Cannot get the content of the http entity: " + e.getMessage());
      throw new TranslationException("Cannot get the content of the http entity", e);
    } finally {
      try {
        // ensure all content has been consumed, so that the
        // underlying connection could be re-used
        EntityUtils.consume(httpEntity);
      } catch (IOException e) {

      }
    }

    return payload;
  }

  /**
   * Gets the coap request. Creates the CoAP request from the HTTP method and
   * mapping it through the properties file. The uri is translated using
   * regular expressions, the uri format expected is either the embedded
   * mapping (http://proxyname.domain:80/proxy/coapserver:5683/resource
   * converted in coap://coapserver:5683/resource) or the standard uri to
   * indicate a local request not to be forwarded. The method uses a decoder
   * to translate the application/x-www-form-urlencoded format of the uri. The
   * CoAP options are set translating the headers. If the HTTP message has an
   * enclosing entity, it is converted to create the payload of the CoAP
   * message; finally the content-type is set accordingly to the header and to
   * the entity type.
   *
   * @param httpRequest
   *            the http request
   * @param proxyResource
   *            the proxy resource, if present in the uri, indicates the need
   *            of forwarding for the current request
   * @param proxyingEnabled
   *            TODO
   *
   *
   * @return the coap request * @throws TranslationException the translation
   *         exception
   */
  public static Request getCoapRequest(HttpRequest httpRequest, String proxyResource, boolean proxyingEnabled) throws TranslationException {
    if (httpRequest == null) {
      throw new IllegalArgumentException("httpRequest == null");
    }
    if (proxyResource == null) {
      throw new IllegalArgumentException("proxyResource == null");
    }

    // get the http method
    String httpMethod = httpRequest.getRequestLine().getMethod().toLowerCase();

    // get the coap method
    String coapMethodString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_HTTP_METHOD + httpMethod);
    if (coapMethodString == null || coapMethodString.contains("error")) {
      throw new InvalidMethodException(httpMethod + " method not mapped");
    }

    int coapMethod = 0;
    try {
      coapMethod = Integer.parseInt(coapMethodString.trim());
    } catch (NumberFormatException e) {
      LOGGER.warning("Cannot convert the http method in coap method: " + e);
      throw new TranslationException("Cannot convert the http method in coap method", e);
    }

    // create the request
//    Request coapRequest = Request.getRequestForMethod(coapMethod);
    Request coapRequest = new Request(Code.valueOf(coapMethod));

    // get the uri
    String uriString = httpRequest.getRequestLine().getUri();
    // remove the initial "/"
    uriString = uriString.substring(1);

    // decode the uri to translate the application/x-www-form-urlencoded
    // format
    try {
      uriString = URLDecoder.decode(uriString, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      LOGGER.warning("Failed to decode the uri: " + e.getMessage());
      throw new TranslationException("Failed decoding the uri: " + e.getMessage());
    } catch (Throwable e) {
      LOGGER.warning("Malformed uri: " + e.getMessage());
      throw new InvalidFieldException("Malformed uri: " + e.getMessage());
    }

    // if the uri contains the proxy resource name, the request should be
    // forwarded and it is needed to get the real requested coap server's
    // uri
    // e.g.:
    // /proxy/vslab-dhcp-17.inf.ethz.ch:5684/helloWorld
    // proxy resource: /proxy
    // coap server: vslab-dhcp-17.inf.ethz.ch:5684
    // coap resource: helloWorld
    if (uriString.matches(".?" + proxyResource + ".*")) {

      // find the first occurrence of the proxy resource
      int index = uriString.indexOf(proxyResource);
      // delete the slash
      index = uriString.indexOf('/', index);
      uriString = uriString.substring(index + 1);

      if (proxyingEnabled) {
        // if the uri hasn't the indication of the scheme, add it
        if (!uriString.matches("^coap://.*")) {
          uriString = "coap://" + uriString;
        }

        // the uri will be set as a proxy-uri option
        // set the proxy-uri option to allow the lower layers to underes
//        Option proxyUriOption = new Option(uriString, OptionRegistry.PROXY_URI);
//        coapRequest.addOption(proxyUriOption);
        coapRequest.getOptions().setProxyURI(uriString);
      } else {
        coapRequest.setURI(uriString);
      }

      // set the proxy as the sender to receive the response correctly
      try {
        // TODO check with multihomed hosts
        InetAddress localHostAddress = InetAddress.getLocalHost();
//        InetSocketAddress localHostEndpoint = new InetSocketAddress(localHostAddress);
//        coapRequest.setPeerAddress(localHostEndpoint);
        coapRequest.setDestination(localHostAddress);
        // TODO: setDestinationPort???
      } catch (UnknownHostException e) {
        LOGGER.warning("Cannot get the localhost address: " + e.getMessage());
        throw new TranslationException("Cannot get the localhost address: " + e.getMessage());
      }
    } else {
      // if the uri does not contains the proxy resource, it means the
      // request is local to the proxy and it shouldn't be forwarded

      // set the uri string as uri-path option
//      Option uriPathOption = new Option(uriString, OptionRegistry.URI_PATH);
//      coapRequest.setOption(uriPathOption);
      coapRequest.getOptions().setURIPath(uriString);
    }

    // translate the http headers in coap options
    List<Option> coapOptions = getCoapOptions(httpRequest.getAllHeaders());
//    coapRequest.setOptions(coapOptions);
    for (Option option:coapOptions)
      coapRequest.getOptions().addOption(option);

    // set the payload if the http entity is present
    if (httpRequest instanceof HttpEntityEnclosingRequest) {
      HttpEntity httpEntity = ((HttpEntityEnclosingRequest) httpRequest).getEntity();

      // translate the http entity in coap payload
      byte[] payload = getCoapPayload(httpEntity);
      coapRequest.setPayload(payload);

      // set the content-type
      int coapContentType = getCoapMediaType(httpRequest);
      coapRequest.getOptions().setContentFormat(coapContentType);
    }

    return coapRequest;
  }

  /**
   * Gets the CoAP response from an incoming HTTP response. No null value is
   * returned. The response is created from a the mapping of the HTTP response
   * code retrieved from the properties file. If the code is 204, which has
   * multiple meaning, the mapping is handled looking on the request method
   * that has originated the response. The options are set thorugh the HTTP
   * headers and the option max-age, if not indicated, is set to the default
   * value (60 seconds). if the response has an enclosing entity, it is mapped
   * to a CoAP payload and the content-type of the CoAP message is set
   * properly.
   *
   * @param httpResponse
   *            the http response
   * @param coapRequest
   *
   *
   * @return the coap response * @throws TranslationException the translation
   *         exception
   */
  public static Response getCoapResponse(HttpResponse httpResponse, Request coapRequest) throws TranslationException {
    if (httpResponse == null) {
      throw new IllegalArgumentException("httpResponse == null");
    }
    if (coapRequest == null) {
      throw new IllegalArgumentException("coapRequest == null");
    }

    // get/set the response code
    int httpCode = httpResponse.getStatusLine().getStatusCode();
    ResponseCode coapCode;
    Code coapMethod = coapRequest.getCode();

    // the code 204-"no content" should be managed
    // separately because it can be mapped to different coap codes
    // depending on the request that has originated the response
    if (httpCode == HttpStatus.SC_NO_CONTENT) {
      if (coapMethod == Code.DELETE) {
        coapCode = ResponseCode.DELETED;
      } else {
        coapCode = ResponseCode.CHANGED;
      }
    } else {
      // get the translation from the property file
      String coapCodeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_HTTP_CODE + httpCode);

      if (coapCodeString == null || coapCodeString.isEmpty()) {
        LOGGER.warning("coapCodeString == null");
        throw new TranslationException("coapCodeString == null");
      }

      try {
        coapCode = ResponseCode.valueOf(Integer.parseInt(coapCodeString.trim()));
      } catch (NumberFormatException e) {
        LOGGER.warning("Cannot convert the status code in number: " + e.getMessage());
        throw new TranslationException("Cannot convert the status code in number", e);
      }
    }

    // create the coap reaponse
    Response coapResponse = new Response(coapCode);

    // translate the http headers in coap options
    List<Option> coapOptions = getCoapOptions(httpResponse.getAllHeaders());
//    coapResponse.addop(coapOptions);
    for (Option option:coapOptions)
      coapResponse.getOptions().addOption(option);

    // the response should indicate a max-age value (CoAP 10.1.1)
//    if (coapResponse.getFirstOption(OptionRegistry.MAX_AGE) == null) {
    if (!coapResponse.getOptions().hasMaxAge()) {
      // The Max-Age Option for responses to POST, PUT or DELETE requests
      // should always be set to 0 (draft-castellani-core-http-mapping).
      if (coapMethod == Code.GET) {
        coapResponse.getOptions().setMaxAge(OptionNumberRegistry.DEFAULT_MAX_AGE);
      } else {
        coapResponse.getOptions().setMaxAge(0);
      }
    }

    // get the entity
    HttpEntity httpEntity = httpResponse.getEntity();
    if (httpEntity != null) {
      // translate the http entity in coap payload
      byte[] payload = getCoapPayload(httpEntity);
      if (payload != null && payload.length > 0) {
        coapResponse.setPayload(payload);

        // set the content-type
        int coapContentType = getCoapMediaType(httpResponse);
        coapResponse.getOptions().setContentFormat(coapContentType);
      }
    }

    return coapResponse;
  }

  /**
   * Generates an HTTP entity starting from a CoAP request. If the coap
   * message has no payload, it returns a null http entity. It takes the
   * payload from the CoAP message and encapsulates it in an entity. If the
   * content-type is recognized, and a mapping is present in the properties
   * file, it is translated to the correspondent in HTTP, otherwise it is set
   * to application/octet-stream. If the content-type has a charset, namely it
   * is printable, the payload is encapsulated in a StringEntity, if not it a
   * ByteArrayEntity is used.
   *
   *
   * @param coapMessage
   *            the coap message
   *
   *
   * @return null if the request has no payload * @throws TranslationException
   *         the translation exception
   */
  public static HttpEntity getHttpEntity(Message coapMessage) throws TranslationException {
    if (coapMessage == null) {
      throw new IllegalArgumentException("coapMessage == null");
    }

    // the result
    HttpEntity httpEntity = null;

    // check if coap request has a payload
    byte[] payload = coapMessage.getPayload();
    if (payload != null && payload.length != 0) {

      // get the coap content-type
//      Integer coapContentType = coapMessage.getOptions().getContentFormat();
      ContentType contentType = null;

      // if the content type is not set, translate with octect-stream
//      if (coapContentType == MediaTypeRegistry.UNDEFINED) {
      if (! coapMessage.getOptions().hasContentFormat()) {
        contentType = ContentType.APPLICATION_OCTET_STREAM;
      } else {
        int coapContentType = coapMessage.getOptions().getContentFormat();
        // search for the media type inside the property file
        String coapContentTypeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_COAP_MEDIA + coapContentType);

        // if the content-type has not been found in the property file,
        // try to get its string value (expressed in mime type)
        if (coapContentTypeString == null || coapContentTypeString.isEmpty()) {
          coapContentTypeString = MediaTypeRegistry.toString(coapContentType);

          // if the coap content-type is printable, it is needed to
          // set the default charset (i.e., UTF-8)
          if (MediaTypeRegistry.isPrintable(coapContentType)) {
            coapContentTypeString += "; charset=UTF-8";
          }
        }

        // parse the content type
        try {
          contentType = ContentType.parse(coapContentTypeString);
//        } catch (ParseException e) {
//          LOGGER.finer("Cannot convert string to ContentType: " + e.getMessage());
//          contentType = ContentType.APPLICATION_OCTET_STREAM;
        } catch (UnsupportedCharsetException e) {
          LOGGER.finer("Cannot convert string to ContentType: " + e.getMessage());
          contentType = ContentType.APPLICATION_OCTET_STREAM;
        }
      }

      // get the charset
      Charset charset = contentType.getCharset();
      // if there is a charset, means that the content is not binary
      if (charset != null) {

        // according to the class ContentType the default content-type
        // with
        // UTF-8 charset is application/json. If the content-type
        // parsed is different, or is not iso encoded, it is needed a
        // translation
        Charset isoCharset = ISO_8859_1;
        if (!charset.equals(isoCharset) && contentType != ContentType.APPLICATION_JSON) {
          byte[] newPayload = changeCharset(payload, charset, isoCharset);

          // since ISO-8859-1 is a subset of UTF-8, it is needed to
          // check if the mapping could be accomplished, only if the
          // operation is succesful the payload and the charset should
          // be changed
          if (newPayload != null) {
            payload = newPayload;
            // if the charset is changed, also the entire
            // content-type must change
            contentType = ContentType.create(contentType.getMimeType(), isoCharset);
          }
        }

        // create the content
        String payloadString = new String(payload, contentType.getCharset());

        // create the entity
        httpEntity = new StringEntity(payloadString, contentType);
      } else {
        // create the entity
        httpEntity = new ByteArrayEntity(payload);
      }

      // set the content-type
      ((AbstractHttpEntity) httpEntity).setContentType(contentType.toString());
    } // if (payload != null && payload.length != 0)

    return httpEntity;
  }

  /**
   * Gets the http headers from a list of CoAP options. The method iterates
   * over the list looking for a translation of each option in the properties
   * file, this process ignores the proxy-uri and the content-type because
   * they are managed differently. If a mapping is present, the content of the
   * option is mapped to a string accordingly to its original format and set
   * as the content of the header.
   *
   *
   * @param optionList
   *            the coap message
   *
   * @return Header[]
   */
  public static Header[] getHttpHeaders(List<Option> optionList) {
    if (optionList == null) {
      throw new IllegalArgumentException("coapMessage == null");
    }

    List<Header> headers = new LinkedList<Header>();

    // iterate over each option
    for (Option option : optionList) {
      // skip content-type because it should be translated while handling
      // the payload; skip proxy-uri because it has to be translated in a
      // different way
      int optionNumber = option.getNumber();
      if (optionNumber != OptionRegistry.CONTENT_FORMAT && optionNumber != OptionRegistry.PROXY_URI) {
        // get the mapping from the property file
        String headerName = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_COAP_OPTION + optionNumber);

        // set the header
        if (headerName != null && !headerName.isEmpty()) {
          // format the value
          String stringOptionValue = null;
          if (OptionNumberRegistry.getFormatByNr(optionNumber) == optionFormats.STRING) {
            stringOptionValue = option.getStringValue();
          } else if (OptionNumberRegistry.getFormatByNr(optionNumber) == optionFormats.INTEGER) {
            stringOptionValue = Integer.toString(option.getIntegerValue());
          } else if (OptionNumberRegistry.getFormatByNr(optionNumber) == optionFormats.OPAQUE) {
            stringOptionValue = new String(option.getValue());
          } else {
            // if the option is not formattable, skip it
            continue;
          }

          // custom handling for max-age
          // format: cache-control: max-age=60
          if (optionNumber == OptionRegistry.MAX_AGE) {
            stringOptionValue = "max-age=" + stringOptionValue;
          }

          Header header = new BasicHeader(headerName, stringOptionValue);
          headers.add(header);
        }
      }
    }

    return headers.toArray(new Header[0]);
  }

  /**
   * Gets the http request starting from a CoAP request. The method creates
   * the HTTP request through its request line. The request line is built with
   * the uri coming from the string representing the CoAP method and the uri
   * obtained from the proxy-uri option. If a payload is provided, the HTTP
   * request encloses an HTTP entity and consequently the content-type is set.
   * Finally, the CoAP options are mapped to the HTTP headers.
   *
   * @param coapRequest
   *            the coap request
   *
   *
   *
   * @return the http request * @throws TranslationException the translation
   *         exception * @throws URISyntaxException the uRI syntax exception
   */
  public static HttpRequest getHttpRequest(Request coapRequest) throws TranslationException {
    if (coapRequest == null) {
      throw new IllegalArgumentException("coapRequest == null");
    }

    HttpRequest httpRequest = null;

    // get the coap method
//    String coapMethod = CodeRegistry.toString(coapRequest.getCode());
    String coapMethod = null;
    switch (coapRequest.getCode()) {
    case GET: coapMethod = "GET"; break;
    case POST: coapMethod = "POST"; break;
    case PUT: coapMethod = "PUT"; break;
    case DELETE: coapMethod = "DELETE"; break;
    }

    // get the proxy-uri
    URI proxyUri;
    try {
      /*
       * The new draft (14) only allows one proxy-uri option. Thus, this
       * code segment has changed.
       */
      String proxyUriString = URLDecoder.decode(
          coapRequest.getOptions().getProxyURI(), "UTF-8");
      proxyUri = new URI(proxyUriString);
    } catch (UnsupportedEncodingException e) {
      LOGGER.warning("UTF-8 do not support this encoding: " + e);
      throw new TranslationException("UTF-8 do not support this encoding", e);
    } catch (URISyntaxException e) {
      LOGGER.warning("Cannot translate the server uri" + e);
      throw new InvalidFieldException("Cannot get the proxy-uri from the coap message", e);
    }
   
//    if (proxyUri == null) {
//      LOGGER.warning("proxyUri == null");
//      throw new InvalidFieldException("proxyUri == null");
//    }

    // create the requestLine
    RequestLine requestLine = new BasicRequestLine(coapMethod, proxyUri.toString(), HttpVersion.HTTP_1_1);

    // get the http entity
    HttpEntity httpEntity = getHttpEntity(coapRequest);

    // create the http request
    if (httpEntity == null) {
      httpRequest = new BasicHttpRequest(requestLine);
    } else {
      httpRequest = new BasicHttpEntityEnclosingRequest(requestLine);
      ((HttpEntityEnclosingRequest) httpRequest).setEntity(httpEntity);

      // get the content-type from the entity and set the header
      ContentType contentType = ContentType.get(httpEntity);
      httpRequest.setHeader("content-type", contentType.toString());
    }

    // set the headers
    Header[] headers = getHttpHeaders(coapRequest.getOptions().asSortedList());
    for (Header header : headers) {
      httpRequest.addHeader(header);
    }

    return httpRequest;
  }
 
  /**
   * Sets the parameters of the incoming http response from a CoAP response.
   * The status code is mapped through the properties file and is set through
   * the StatusLine. The options are translated to the corresponding headers
   * and the max-age (in the header cache-control) is set to the default value
   * (60 seconds) if not already present. If the request method was not HEAD
   * and the coap response has a payload, the entity and the content-type are
   * set in the http response.
   *
   * @param coapResponse
   *            the coap response
   * @param httpResponse
   *
   *
   *
   * @param httpRequest
   *            HttpRequest
   * @throws TranslationException
   *             the translation exception
   */
  public static void getHttpResponse(HttpRequest httpRequest, Response coapResponse, HttpResponse httpResponse) throws TranslationException {
    if (httpRequest == null) {
      throw new IllegalArgumentException("httpRequest == null");
    }
    if (coapResponse == null) {
      throw new IllegalArgumentException("coapResponse == null");
    }
    if (httpResponse == null) {
      throw new IllegalArgumentException("httpResponse == null");
    }

    // get/set the response code
    ResponseCode coapCode = coapResponse.getCode();
    String httpCodeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_COAP_CODE + coapCode.value);

    if (httpCodeString == null || httpCodeString.isEmpty()) {
      LOGGER.warning("httpCodeString == null");
      throw new TranslationException("httpCodeString == null");
    }

    int httpCode = 0;
    try {
      httpCode = Integer.parseInt(httpCodeString.trim());
    } catch (NumberFormatException e) {
      LOGGER.warning("Cannot convert the coap code in http status code" + e);
      throw new TranslationException("Cannot convert the coap code in http status code", e);
    }

    // create the http response and set the status line
    String reason = EnglishReasonPhraseCatalog.INSTANCE.getReason(httpCode, Locale.ENGLISH);
    StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1, httpCode, reason);
    httpResponse.setStatusLine(statusLine);

    // set the headers
    Header[] headers = getHttpHeaders(coapResponse.getOptions().asSortedList());
    httpResponse.setHeaders(headers);

    // set max-age if not already set
    if (!httpResponse.containsHeader("cache-control")) {
      httpResponse.setHeader("cache-control", "max-age=" + Integer.toString(OptionNumberRegistry.DEFAULT_MAX_AGE));
    }

    // get the http entity if the request was not HEAD
    if (!httpRequest.getRequestLine().getMethod().equalsIgnoreCase("head")) {

      // if the content-type is not set in the coap response and if the
      // response contains an error, then the content-type should set to
      // text-plain
      if (coapResponse.getOptions().getContentFormat() == MediaTypeRegistry.UNDEFINED
          && (ResponseCode.isClientError(coapCode)
          || ResponseCode.isServerError(coapCode))) {
        LOGGER.info("Set contenttype to TEXT_PLAIN");
        coapResponse.getOptions().setContentFormat(MediaTypeRegistry.TEXT_PLAIN);
      }

      HttpEntity httpEntity = getHttpEntity(coapResponse);
      if (httpEntity != null) {
        httpResponse.setEntity(httpEntity);

        // get the content-type from the entity and set the header
        ContentType contentType = ContentType.get(httpEntity);
        httpResponse.setHeader("content-type", contentType.toString());
      }
    }
  }

  /**
   * Change charset.
   *
   * @param payload
   *            the payload
   * @param fromCharset
   *            the from charset
   * @param toCharset
   *            the to charset
   *
   *
   * @return the byte[] * @throws TranslationException the translation
   *         exception
   */
  private static byte[] changeCharset(byte[] payload, Charset fromCharset, Charset toCharset) throws TranslationException {
    try {
      // decode with the source charset
      CharsetDecoder decoder = fromCharset.newDecoder();
      CharBuffer charBuffer = decoder.decode(ByteBuffer.wrap(payload));
      decoder.flush(charBuffer);

      // encode to the destination charset
      CharsetEncoder encoder = toCharset.newEncoder();
      ByteBuffer byteBuffer = encoder.encode(charBuffer);
      encoder.flush(byteBuffer);
      payload = byteBuffer.array();
    } catch (UnmappableCharacterException e) {
      // thrown when an input character (or byte) sequence is valid but
      // cannot be mapped to an output byte (or character) sequence.
      // If the character sequence starting at the input buffer's current
      // position cannot be mapped to an equivalent byte sequence and the
      // current unmappable-character
      LOGGER.finer("Charset translation: cannot mapped to an output char byte: " + e.getMessage());
      return null;
    } catch (CharacterCodingException e) {
      LOGGER.warning("Problem in the decoding/encoding charset: " + e.getMessage());
      throw new TranslationException("Problem in the decoding/encoding charset", e);
    }

    return payload;
  }

  /**
   * The Constructor is private because the class is an helper class and
   * cannot be instantiated.
   */
  private HttpTranslator() {

  }

}
TOP

Related Classes of ch.ethz.inf.vs.californium.resources.proxy.HttpTranslator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.