/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.brooklyn.util.core.text; import java.io.ByteArrayInputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.LinkedHashMap; import java.util.Map; import org.apache.brooklyn.util.exceptions.Exceptions; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; //import com.sun.jersey.core.util.Base64; /** implementation (currently hokey) of RFC-2397 data: URI scheme. * see: http://stackoverflow.com/questions/12353552/any-rfc-2397-data-uri-parser-for-java */ public class DataUriSchemeParser { public static final String PROTOCOL_PREFIX = "data:"; public static final String DEFAULT_MIME_TYPE = "text/plain"; public static final String DEFAULT_CHARSET = "US-ASCII"; private final String url; private int parseIndex = 0; private boolean isParsed = false; private boolean allowMissingComma = false; private boolean allowSlashesAfterColon = false; private boolean allowOtherLaxities = false; private String mimeType; private byte[] data; private Map<String,String> parameters = new LinkedHashMap<String,String>(); public DataUriSchemeParser(String url) { this.url = Preconditions.checkNotNull(url, "url"); } // ---- static conveniences ----- public static String toString(String url) { return new DataUriSchemeParser(url).lax().parse().getDataAsString(); } public static byte[] toBytes(String url) { return new DataUriSchemeParser(url).lax().parse().getData(); } // ---- accessors (once it is parsed) ----------- public String getCharset() { String charset = parameters.get("charset"); if (charset!=null) return charset; return DEFAULT_CHARSET; } public String getMimeType() { assertParsed(); if (mimeType!=null) return mimeType; return DEFAULT_MIME_TYPE; } public Map<String, String> getParameters() { return ImmutableMap.<String, String>copyOf(parameters); } public byte[] getData() { assertParsed(); return data; } public ByteArrayInputStream getDataAsInputStream() { return new ByteArrayInputStream(getData()); } public String getDataAsString() { return new String(getData(), Charset.forName(getCharset())); } // ---- config ------------------ public synchronized DataUriSchemeParser lax() { return allowMissingComma(true).allowSlashesAfterColon(true).allowOtherLaxities(true); } public synchronized DataUriSchemeParser allowMissingComma(boolean allowMissingComma) { assertNotParsed(); this.allowMissingComma = allowMissingComma; return this; } public synchronized DataUriSchemeParser allowSlashesAfterColon(boolean allowSlashesAfterColon) { assertNotParsed(); this.allowSlashesAfterColon = allowSlashesAfterColon; return this; } private synchronized DataUriSchemeParser allowOtherLaxities(boolean allowOtherLaxities) { assertNotParsed(); this.allowOtherLaxities = allowOtherLaxities; return this; } private void assertNotParsed() { if (isParsed) throw new IllegalStateException("Operation not permitted after parsing"); } private void assertParsed() { if (!isParsed) throw new IllegalStateException("Operation not permitted before parsing"); } public synchronized DataUriSchemeParser parse() { try { return parseChecked(); } catch (Exception e) { throw Exceptions.propagate(e); } } public synchronized DataUriSchemeParser parseChecked() throws UnsupportedEncodingException, MalformedURLException { if (isParsed) return this; skipOptional(PROTOCOL_PREFIX); if (allowSlashesAfterColon) while (skipOptional("/")) ; if (allowMissingComma && remainder().indexOf(',')==-1) { mimeType = DEFAULT_MIME_TYPE; parameters.put("charset", DEFAULT_CHARSET); } else { parseMediaType(); parseParameterOrParameterValues(); skipRequired(","); } parseData(); isParsed = true; return this; } private void parseMediaType() throws MalformedURLException { if (remainder().startsWith(";") || remainder().startsWith(",")) return; int slash = remainder().indexOf("/"); if (slash==-1) throw new MalformedURLException("Missing required '/' in MIME type of data: URL"); String type = read(slash); skipRequired("/"); int next = nextSemiOrComma(); String subtype = read(next); mimeType = type+"/"+subtype; } private String read(int next) { String result = remainder().substring(0, next); parseIndex += next; return result; } private int nextSemiOrComma() throws MalformedURLException { int semi = remainder().indexOf(';'); int comma = remainder().indexOf(','); if (semi<0 && comma<0) throw new MalformedURLException("Missing required ',' in data: URL"); if (semi<0) return comma; if (comma<0) return semi; return Math.min(semi, comma); } private void parseParameterOrParameterValues() throws MalformedURLException { while (true) { if (!remainder().startsWith(";")) return; parseIndex++; int eq = remainder().indexOf('='); String word, value; int nextSemiOrComma = nextSemiOrComma(); if (eq==-1 || eq>nextSemiOrComma) { word = read(nextSemiOrComma); value = null; } else { word = read(eq); if (remainder().startsWith("\"")) { // is quoted parseIndex++; int nextUnescapedQuote = nextUnescapedQuote(); value = "\"" + read(nextUnescapedQuote); } else { value = read(nextSemiOrComma()); } } parameters.put(word, value); } } private int nextUnescapedQuote() throws MalformedURLException { int i=0; String r = remainder(); boolean escaped = false; while (i<r.length()) { if (escaped) { escaped = false; } else { if (r.charAt(i)=='"') return i; if (r.charAt(i)=='\\') escaped = true; } i++; } throw new MalformedURLException("Unclosed double-quote in data: URL"); } private void parseData() throws UnsupportedEncodingException, MalformedURLException { if (parameters.containsKey("base64")) { checkNoParamValue("base64"); data = BaseEncoding.base64().decode(remainder()); } else if (parameters.containsKey("base64url")) { checkNoParamValue("base64url"); data = BaseEncoding.base64Url().decode(remainder()); } else { data = URLDecoder.decode(remainder(), getCharset()).getBytes(Charset.forName(getCharset())); } } private void checkNoParamValue(String param) throws MalformedURLException { if (allowOtherLaxities) return; String value = parameters.get(param); if (value!=null) throw new MalformedURLException(param+" parameter must not take a value ("+value+") in data: URL"); } private String remainder() { return url.substring(parseIndex); } private boolean skipOptional(String word) { if (remainder().startsWith(word)) { parseIndex += word.length(); return true; } return false; } private void skipRequired(String word) throws MalformedURLException { if (!remainder().startsWith(word)) throw new MalformedURLException("Missing required '"+word+"' at position "+parseIndex+" of data: URL"); parseIndex += word.length(); } }