/*******************************************************************************
* Copyright (c) 2009, 2010 Fraunhofer IWU and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Fraunhofer IWU - initial API and implementation
*******************************************************************************/
package net.enilink.komma.model.base;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import net.enilink.komma.core.URI;
import net.enilink.komma.model.IContentHandler;
import net.enilink.komma.model.IModel;
import net.enilink.komma.model.IURIConverter;
import net.enilink.komma.model.IURIHandler;
import net.enilink.komma.model.ModelUtil;
/**
* An implementation of a {@link IURIHandler URI handler}.
*
*/
public class URIHandler implements IURIHandler {
private final static int TIMEOUT_HARD_IN_MS = 5000;
private final static int TIMEOUT_SOFT_IN_MS = TIMEOUT_HARD_IN_MS - 100;
/**
* This implementation always returns true; clients are generally expected
* to override this.
*/
public boolean canHandle(URI uri) {
return true;
}
/**
* Returns the value of the {@link URIConverter#OPTION_URI_CONVERTER URI
* converter option}.
*
* @param options
* the options in which to look for the URI converter.
* @return the value of the URI converter option.
*/
protected IURIConverter getURIConverter(Map<?, ?> options) {
return (IURIConverter) options.get(IURIConverter.OPTION_URI_CONVERTER);
}
/**
* Returns the value of the {@link URIConverter#OPTION_RESPONSE response
* option}.
*
* @param options
* the options in which to look for the response option.
* @return the value of the response option.
*/
@SuppressWarnings("unchecked")
protected Map<Object, Object> getResponse(Map<?, ?> options) {
return (Map<Object, Object>) options.get(IURIConverter.OPTION_RESPONSE);
}
/**
* Returns the value of the {@link URIConverter#OPTION_REQUESTED_ATTRIBUTES
* requested attributes option}.
*
* @param options
* the options in which to look for the requested attributes
* option.
* @return the value of the requested attributes option.
*/
@SuppressWarnings("unchecked")
protected Set<String> getRequestedAttributes(Map<?, ?> options) {
return (Set<String>) options
.get(IURIConverter.OPTION_REQUESTED_ATTRIBUTES);
}
/**
* Creates an output stream for the URI, assuming it's a URL, and returns
* it. Specialized support is provided for HTTP URLs.
*
* @return an open output stream.
* @exception IOException
* if there is a problem obtaining an open output stream.
*/
public OutputStream createOutputStream(URI uri, Map<?, ?> options)
throws IOException {
try {
URL url = new URL(uri.toString());
final URLConnection urlConnection = url.openConnection();
urlConnection.setDoOutput(true);
if (urlConnection instanceof HttpURLConnection) {
final HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
httpURLConnection.setRequestMethod("PUT");
return new FilterOutputStream(urlConnection.getOutputStream()) {
@Override
public void close() throws IOException {
super.close();
int responseCode;
try {
responseCode = getResponseCode(httpURLConnection);
} catch (InterruptedException | ExecutionException
| TimeoutException e) {
throw new IOException(e);
}
switch (responseCode) {
case HttpURLConnection.HTTP_OK:
case HttpURLConnection.HTTP_CREATED:
case HttpURLConnection.HTTP_NO_CONTENT: {
break;
}
default: {
throw new IOException(
"PUT failed with HTTP response code "
+ responseCode);
}
}
}
};
} else {
OutputStream result = urlConnection.getOutputStream();
final Map<Object, Object> response = getResponse(options);
if (response != null) {
result = new FilterOutputStream(result) {
@Override
public void close() throws IOException {
try {
super.close();
} finally {
response.put(
IURIConverter.RESPONSE_TIME_STAMP_PROPERTY,
urlConnection.getLastModified());
}
}
};
}
return result;
}
} catch (RuntimeException exception) {
throw new IModel.IOWrappedException(exception);
}
}
protected String acceptHeader() {
StringBuilder accept = new StringBuilder();
for (Map.Entry<String, Double> mimeType : ModelUtil
.getSupportedMimeTypes().entrySet()) {
if (accept.length() > 0) {
accept.append(", ");
}
accept.append(mimeType.getKey()).append("; q=")
.append(String.format("%.2f", mimeType.getValue()));
}
return accept.toString();
}
/**
* Creates an input stream for the URI, assuming it's a URL, and returns it.
*
* @return an open input stream.
* @exception IOException
* if there is a problem obtaining an open input stream.
*/
public InputStream createInputStream(URI uri, Map<?, ?> options)
throws IOException {
try {
URL url = new URL(uri.toString());
final URLConnection urlConnection = url.openConnection();
urlConnection.setRequestProperty("Accept", acceptHeader());
InputStream result = getInputStream(urlConnection);
Map<Object, Object> response = getResponse(options);
if (response != null) {
response.put(IURIConverter.RESPONSE_TIME_STAMP_PROPERTY,
urlConnection.getLastModified());
if (urlConnection.getContentType() != null) {
response.put(
IURIConverter.RESPONSE_MIME_TYPE_PROPERTY,
urlConnection.getContentType()
.replaceAll(";.*$", "").trim());
}
}
return result;
} catch (RuntimeException | ExecutionException | InterruptedException
| TimeoutException exception) {
throw new IModel.IOWrappedException(exception);
}
}
/**
* Only HTTP connections support delete.
*/
public void delete(URI uri, Map<?, ?> options) throws IOException {
try {
URL url = new URL(uri.toString());
URLConnection urlConnection = url.openConnection();
urlConnection.setDoOutput(true);
if (urlConnection instanceof HttpURLConnection) {
final HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
httpURLConnection.setRequestMethod("DELETE");
int responseCode = getResponseCode(httpURLConnection);
switch (responseCode) {
case HttpURLConnection.HTTP_OK:
case HttpURLConnection.HTTP_ACCEPTED:
case HttpURLConnection.HTTP_NO_CONTENT: {
break;
}
default: {
throw new IOException(
"DELETE failed with HTTP response code "
+ responseCode);
}
}
} else {
throw new IOException("Delete is not supported for " + uri);
}
} catch (RuntimeException | ExecutionException | InterruptedException
| TimeoutException exception) {
throw new IModel.IOWrappedException(exception);
}
}
/**
* This implementation delegates to the {@link #getURIConverter(Map) URI
* converter}'s {@link URIConverter#getContentHandlers() content handlers}.
*/
public Map<String, ?> contentDescription(URI uri, Map<?, ?> options)
throws IOException {
IURIConverter uriConverter = (IURIConverter) options
.get(IURIConverter.OPTION_URI_CONVERTER);
InputStream inputStream = null;
Map<String, ?> result = null;
Map<Object, Object> context = new HashMap<Object, Object>();
try {
for (IContentHandler contentHandler : uriConverter
.getContentHandlers()) {
if (contentHandler.canHandle(uri)) {
if (inputStream == null) {
try {
inputStream = createInputStream(uri, options);
} catch (IOException exception) {
inputStream = new ByteArrayInputStream(new byte[0]);
}
if (!inputStream.markSupported()) {
inputStream = new BufferedInputStream(inputStream);
}
inputStream.mark(Integer.MAX_VALUE);
} else {
inputStream.reset();
}
context.put(IURIConverter.ATTRIBUTE_MIME_TYPE, options
.get(IURIConverter.RESPONSE_MIME_TYPE_PROPERTY));
Map<String, ?> contentDescription = contentHandler
.contentDescription(uri, inputStream, options,
context);
switch ((IContentHandler.Validity) contentDescription
.get(IContentHandler.VALIDITY_PROPERTY)) {
case VALID: {
return contentDescription;
}
case INDETERMINATE: {
if (result == null) {
result = contentDescription;
}
break;
}
case INVALID: {
break;
}
}
}
}
} finally {
if (inputStream != null) {
inputStream.close();
}
}
return result == null ? IContentHandler.INVALID_CONTENT_DESCRIPTION
: result;
}
/**
* If a stream can be created the file exists. Specialized support is
* provided for HTTP connections to avoid fetching the whole stream in that
* case.
*/
public boolean exists(URI uri, Map<?, ?> options) {
try {
URL url = new URL(uri.toString());
URLConnection urlConnection = url.openConnection();
urlConnection.setRequestProperty("Accept", acceptHeader());
if (urlConnection instanceof HttpURLConnection) {
HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
httpURLConnection.setRequestMethod("HEAD");
int responseCode = getResponseCode(httpURLConnection);
// TODO
// I'm concerned that folders will often return 401 or even 403.
// So should we consider something to exist even though access
// if unauthorized or forbidden?
//
return responseCode == HttpURLConnection.HTTP_OK;
} else {
InputStream inputStream = urlConnection.getInputStream();
inputStream.close();
return true;
}
} catch (Throwable exception) {
return false;
}
}
public Map<String, ?> getAttributes(URI uri, Map<?, ?> options) {
Map<String, Object> result = new HashMap<String, Object>();
Set<String> requestedAttributes = getRequestedAttributes(options);
try {
URL url = new URL(uri.toString());
URLConnection urlConnection = null;
if (requestedAttributes == null
|| requestedAttributes
.contains(IURIConverter.ATTRIBUTE_READ_ONLY)) {
urlConnection = url.openConnection();
urlConnection.setRequestProperty("Accept", acceptHeader());
if (urlConnection instanceof HttpURLConnection) {
HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
httpURLConnection.setRequestMethod("OPTIONS");
int responseCode = getResponseCode(httpURLConnection);
if (responseCode == HttpURLConnection.HTTP_OK) {
String allow = httpURLConnection
.getHeaderField("Allow");
result.put(IURIConverter.ATTRIBUTE_READ_ONLY,
allow == null || !allow.contains("PUT"));
}
urlConnection = null;
} else {
result.put(IURIConverter.ATTRIBUTE_READ_ONLY, true);
}
}
if (requestedAttributes == null
|| requestedAttributes
.contains(IURIConverter.ATTRIBUTE_TIME_STAMP)
|| requestedAttributes
.contains(IURIConverter.ATTRIBUTE_LENGTH)
|| requestedAttributes
.contains(IURIConverter.ATTRIBUTE_MIME_TYPE)) {
if (urlConnection == null) {
urlConnection = url.openConnection();
urlConnection.setRequestProperty("Accept", acceptHeader());
if (urlConnection instanceof HttpURLConnection) {
HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
httpURLConnection.setRequestMethod("HEAD");
getResponseCode(httpURLConnection);
}
}
if (requestedAttributes == null
|| requestedAttributes
.contains(IURIConverter.ATTRIBUTE_TIME_STAMP)) {
if (urlConnection.getHeaderField("last-modified") != null) {
result.put(IURIConverter.ATTRIBUTE_TIME_STAMP,
urlConnection.getLastModified());
}
}
if (requestedAttributes == null
|| requestedAttributes
.contains(IURIConverter.ATTRIBUTE_LENGTH)) {
if (urlConnection.getHeaderField("content-length") != null) {
result.put(IURIConverter.ATTRIBUTE_LENGTH,
urlConnection.getContentLength());
}
}
if (requestedAttributes == null
|| requestedAttributes
.contains(IURIConverter.ATTRIBUTE_MIME_TYPE)) {
String contentType = urlConnection.getContentType();
if (contentType != null) {
result.put(IURIConverter.ATTRIBUTE_MIME_TYPE,
contentType.replaceAll(";.*$", "").trim());
}
}
}
} catch (IOException | ExecutionException | InterruptedException
| TimeoutException exception) {
// Ignore exceptions.
}
return result;
}
private int getResponseCode(final HttpURLConnection connection)
throws InterruptedException, ExecutionException, TimeoutException {
FutureTask<Integer> futureTask = new FutureTask<>(
new Callable<Integer>() {
@Override
public Integer call() throws Exception {
setupTimeout(connection);
return connection.getResponseCode();
}
});
new Thread(futureTask).start();
return futureTask.get(TIMEOUT_HARD_IN_MS, TimeUnit.MILLISECONDS);
}
private InputStream getInputStream(final URLConnection connection)
throws InterruptedException, ExecutionException, TimeoutException {
FutureTask<InputStream> futureTask = new FutureTask<>(
new Callable<InputStream>() {
@Override
public InputStream call() throws Exception {
setupTimeout(connection);
return connection.getInputStream();
}
});
new Thread(futureTask).start();
return futureTask.get(TIMEOUT_HARD_IN_MS, TimeUnit.MILLISECONDS);
}
private void setupTimeout(URLConnection connection) {
connection.setConnectTimeout(TIMEOUT_SOFT_IN_MS);
connection.setReadTimeout(TIMEOUT_SOFT_IN_MS);
}
public void setAttributes(URI uri, Map<String, ?> attributes,
Map<?, ?> options) throws IOException {
// We can't update any properties via just a URL connection.
}
}