/*
* R Service Bus
*
* Copyright (c) Copyright of Open Analytics NV, 2010-2015
*
* ===========================================================================
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.openanalytics.rsb;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.UUID;
import java.util.regex.Pattern;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.activation.MimetypesFileTypeMap;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;
import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
import org.eclipse.core.runtime.CoreException;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroup;
import de.walware.rj.data.RObject;
import de.walware.rj.servi.RServi;
import de.walware.rj.services.FunctionCall;
import eu.openanalytics.rsb.rest.types.ErrorResult;
import eu.openanalytics.rsb.rest.types.ObjectFactory;
/**
* Shared utilities.
*
* @author "OpenAnalytics <rsb.development@openanalytics.eu>"
*/
public abstract class Util
{
private final static Pattern APPLICATION_NAME_VALIDATOR = Pattern.compile("\\w+");
private final static ObjectMapper JSON_OBJECT_MAPPER = new ObjectMapper();
private final static ObjectMapper PRETTY_JSON_OBJECT_MAPPER = new ObjectMapper();
private final static JAXBContext ERROR_RESULT_JAXB_CONTEXT;
private final static DatatypeFactory XML_DATATYPE_FACTORY;
private static final MimetypesFileTypeMap MIMETYPES_FILETYPE_MAP = new MimetypesFileTypeMap();
private static final String DEFAULT_FILE_EXTENSION = "dat";
private static final Map<String, String> DEFAULT_FILE_EXTENSIONS = new HashMap<String, String>();
private static final STGroup $_STRING_TEMPLATE_GROUP = new STGroup('$', '$');
static
{
PRETTY_JSON_OBJECT_MAPPER.configure(Feature.INDENT_OUTPUT, true);
PRETTY_JSON_OBJECT_MAPPER.configure(Feature.SORT_PROPERTIES_ALPHABETICALLY, true);
PRETTY_JSON_OBJECT_MAPPER.setSerializationInclusion(Inclusion.NON_NULL);
try
{
ERROR_RESULT_JAXB_CONTEXT = JAXBContext.newInstance(ErrorResult.class);
XML_DATATYPE_FACTORY = DatatypeFactory.newInstance();
MIMETYPES_FILETYPE_MAP.addMimeTypes(Constants.JSON_CONTENT_TYPE + " json\n"
+ Constants.XML_CONTENT_TYPE + " xml\n"
+ Constants.TEXT_CONTENT_TYPE + " txt\n"
+ Constants.TEXT_CONTENT_TYPE + " R\n"
+ Constants.TEXT_CONTENT_TYPE + " Rnw\n"
+ Constants.PDF_CONTENT_TYPE + " pdf\n"
+ Constants.ZIP_CONTENT_TYPE + " zip");
}
catch (final Exception e)
{
throw new IllegalStateException(e);
}
DEFAULT_FILE_EXTENSIONS.put(Constants.JSON_CONTENT_TYPE, "json");
DEFAULT_FILE_EXTENSIONS.put(Constants.XML_CONTENT_TYPE, "xml");
DEFAULT_FILE_EXTENSIONS.put(Constants.TEXT_CONTENT_TYPE, "txt");
DEFAULT_FILE_EXTENSIONS.put(Constants.PDF_CONTENT_TYPE, "pdf");
DEFAULT_FILE_EXTENSIONS.put(Constants.ZIP_CONTENT_TYPE, "zip");
}
public final static ObjectFactory REST_OBJECT_FACTORY = new ObjectFactory();
public final static eu.openanalytics.rsb.soap.types.ObjectFactory SOAP_OBJECT_FACTORY = new eu.openanalytics.rsb.soap.types.ObjectFactory();
private Util()
{
throw new UnsupportedOperationException("do not instantiate");
}
/**
* Creates a new {@link ST} configured with a $..$ group.
*
* @param template
* @return a new {@link ST}
*/
public static ST newStringTemplate(final String template)
{
return new ST($_STRING_TEMPLATE_GROUP, template);
}
/**
* Returns the must probable resource type for a MimeType.
*
* @param mimeType
* @return
*/
public static String getResourceType(final MimeType mimeType)
{
final String result = DEFAULT_FILE_EXTENSIONS.get(mimeType.toString());
return result != null ? result : DEFAULT_FILE_EXTENSION;
}
/**
* Returns the must probable content type for a file.
*
* @param file
* @return "application/octet-stream" if unknown.
*/
public static String getContentType(final File file)
{
return MIMETYPES_FILETYPE_MAP.getContentType(file);
}
/**
* Returns the must probable mime type for a file.
*
* @param file
* @return {@link eu.openanalytics.rsb.Constants#DEFAULT_MIME_TYPE} if unknown.
*/
public static MimeType getMimeType(final File file)
{
try
{
return new MimeType(getContentType(file));
}
catch (final MimeTypeParseException mtpe)
{
return Constants.DEFAULT_MIME_TYPE;
}
}
/**
* Builds a result URI.
*
* @param applicationName
* @param jobId
* @param httpHeaders
* @param uriInfo
* @return
* @throws URISyntaxException
*/
public static URI buildResultUri(final String applicationName,
final String jobId,
final HttpHeaders httpHeaders,
final UriInfo uriInfo) throws URISyntaxException
{
return getUriBuilder(uriInfo, httpHeaders).path(Constants.RESULTS_PATH)
.path(applicationName)
.path(jobId)
.build();
}
/**
* Builds a data directory URI.
*
* @param applicationName
* @param jobId
* @param httpHeaders
* @param uriInfo
* @return
* @throws URISyntaxException
*/
public static URI buildDataDirectoryUri(final HttpHeaders httpHeaders,
final UriInfo uriInfo,
final String... directoryPathElements) throws URISyntaxException
{
UriBuilder uriBuilder = getUriBuilder(uriInfo, httpHeaders).path(Constants.DATA_DIR_PATH);
for (final String directoryPathElement : directoryPathElements)
{
if (StringUtils.isNotEmpty(directoryPathElement))
{
uriBuilder = uriBuilder.path(directoryPathElement);
}
}
return uriBuilder.build();
}
/**
* Validates that the passed string is a valid application name.
*
* @param name
* @return
*/
public static boolean isValidApplicationName(final String name)
{
return StringUtils.isNotBlank(name) && APPLICATION_NAME_VALIDATOR.matcher(name).matches();
}
/**
* Extracts an UriBuilder for the current request, taking into account the possibility of
* header-based URI override.
*
* @param uriInfo
* @param httpHeaders
* @return
* @throws URISyntaxException
*/
public static UriBuilder getUriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders)
throws URISyntaxException
{
final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
final List<String> hosts = httpHeaders.getRequestHeader(HttpHeaders.HOST);
if ((hosts != null) && (!hosts.isEmpty()))
{
final String host = hosts.get(0);
uriBuilder.host(StringUtils.substringBefore(host, ":"));
final String port = StringUtils.substringAfter(host, ":");
if (StringUtils.isNotBlank(port))
{
uriBuilder.port(Integer.valueOf(port));
}
}
final String protocol = getSingleHeader(httpHeaders, Constants.FORWARDED_PROTOCOL_HTTP_HEADER);
if (StringUtils.isNotBlank(protocol))
{
uriBuilder.scheme(protocol);
}
return uriBuilder;
}
/**
* Converts a {@link GregorianCalendar} into a {@link XMLGregorianCalendar}
*
* @param calendar
* @return
*/
public static XMLGregorianCalendar convertToXmlDate(final GregorianCalendar calendar)
{
final GregorianCalendar zuluDate = new GregorianCalendar();
zuluDate.setTimeZone(TimeZone.getTimeZone("UTC"));
zuluDate.setTimeInMillis(calendar.getTimeInMillis());
final XMLGregorianCalendar xmlDate = XML_DATATYPE_FACTORY.newXMLGregorianCalendar(zuluDate);
return xmlDate;
}
/**
* Gets the first header of multiple HTTP headers, returning null if no header is found for the
* name.
*
* @param httpHeaders
* @param headerName
* @return
*/
public static String getSingleHeader(final HttpHeaders httpHeaders, final String headerName)
{
final List<String> headers = httpHeaders.getRequestHeader(headerName);
if ((headers == null) || (headers.isEmpty()))
{
return null;
}
return headers.get(0);
}
/**
* Creates a temporary directory. Lifted from: http://stackoverflow.com/questions/
* 617414/create-a-temporary-directory-in-java/617438#617438
*
* @return
* @throws IOException
*/
public static File createTemporaryDirectory(final String type) throws IOException
{
final File temp;
temp = File.createTempFile("rsb_", type);
if (!(temp.delete()))
{
throw new IOException("Could not delete temp file: " + temp.getAbsolutePath());
}
if (!(temp.mkdir()))
{
throw new IOException("Could not create temp directory: " + temp.getAbsolutePath());
}
return (temp);
}
/**
* Marshals an {@link ErrorResult} to XML.
*
* @param errorResult
* @return
*/
public static String toXml(final ErrorResult errorResult)
{
try
{
final Marshaller marshaller = ERROR_RESULT_JAXB_CONTEXT.createMarshaller();
final StringWriter sw = new StringWriter();
marshaller.marshal(errorResult, sw);
return sw.toString();
}
catch (final JAXBException je)
{
final String objectAsString = ToStringBuilder.reflectionToString(errorResult,
ToStringStyle.SHORT_PREFIX_STYLE);
throw new RuntimeException("Failed to XML marshall: " + objectAsString, je);
}
}
/**
* Marshals an {@link Object} to a JSON string.
*
* @param o
* @return
*/
public static String toJson(final Object o)
{
try
{
return PRETTY_JSON_OBJECT_MAPPER.writeValueAsString(o);
}
catch (final IOException ioe)
{
final String objectAsString = ToStringBuilder.reflectionToString(o,
ToStringStyle.SHORT_PREFIX_STYLE);
throw new RuntimeException("Failed to JSON marshall: " + objectAsString, ioe);
}
}
/**
* Marshals an {@link Object} to a pretty-printed JSON file.
*
* @param o
* @throws IOException
*/
public static void toPrettyJsonFile(final Object o, final File f) throws IOException
{
try
{
PRETTY_JSON_OBJECT_MAPPER.writeValue(f, o);
}
catch (final JsonProcessingException jpe)
{
final String objectAsString = ToStringBuilder.reflectionToString(o,
ToStringStyle.SHORT_PREFIX_STYLE);
throw new RuntimeException("Failed to JSON marshall: " + objectAsString, jpe);
}
}
/**
* Unmarshalls a JSON string to a desired type.
*
* @param s
* @return
*/
public static <T> T fromJson(final String s, final Class<T> clazz)
{
try
{
return JSON_OBJECT_MAPPER.readValue(s, clazz);
}
catch (final IOException ioe)
{
throw new RuntimeException("Failed to JSON unmarshall: " + s, ioe);
}
}
/**
* Perform a simple arithmetic operation on R to ensure it responds correctly.
*
* @param rServi
* @return
*/
public static boolean isRResponding(final RServi rServi)
{
try
{
final FunctionCall functionCall = rServi.createFunctionCall("sum");
functionCall.addInt(1);
functionCall.addInt(2);
final RObject result = functionCall.evalData(null);
return result.getData().getInt(0) == 3;
}
catch (final CoreException ce)
{
return false;
}
}
/**
* Creates a new {@link URI} from String, throwing an {@link IllegalArgumentException} in case
* of issue.
*
* @param uri
* @return
*/
public static URI newURI(final String uri)
{
try
{
return new URI(uri);
}
catch (final URISyntaxException urise)
{
throw new IllegalArgumentException(uri + " is not a valid URI", urise);
}
}
/**
* Rename well known meta properties to their canonical names.
*
* @param meta
* @return
*/
public static Map<String, Serializable> normalizeJobMeta(final Map<String, Serializable> meta)
{
final Map<String, Serializable> normalized = new HashMap<String, Serializable>(meta.size());
for (final Entry<String, Serializable> entry : meta.entrySet())
{
final String normalizedName = Constants.WELL_KNOWN_CONFIGURATION_KEYS.get(entry.getKey()
.toLowerCase());
if (normalizedName != null)
{
normalized.put(normalizedName, entry.getValue());
}
else
{
normalized.put(entry.getKey(), entry.getValue());
}
}
return normalized;
}
/**
* Safely decodes a {@link String} into an {@link UUID} instance.
*
* @param uuid a {@link String} the potentially contains a UUID. Can be null.
* @return the decoded UUID or null if the format is invalid.
*/
public static UUID safeUuidFromString(final String uuid)
{
if (StringUtils.isBlank(uuid))
{
return null;
}
try
{
return UUID.fromString(uuid);
}
catch (final RuntimeException re)
{
return null;
}
}
/**
* Null-safe replacement of non-word characters (ie matching <code>\W</code>).
*
* @param source
* @param replacement
* @return
*/
public static String replaceNonWordChars(final String source, final String replacement)
{
if (StringUtils.isEmpty(source))
{
return source;
}
return source.replaceAll("\\W", replacement);
}
}