/** * personium.io * Copyright 2014 FUJITSU LIMITED * * 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 com.fujitsu.dc.common.utils; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Date; import java.util.List; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.CharEncoding; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; /** * 各種ユーティリティ関数を集めたクラス. */ /** * @author shimono */ public final class DcCoreUtils { /** * ログ. */ static Logger log = LoggerFactory.getLogger(DcCoreUtils.class); private DcCoreUtils() { } /** * 独自のHttpヘッダを定数としてこの下に定義します. */ public static class HttpHeaders { /** * Accountのパスワード設定・変更を受け付けるヘッダ. */ public static final String X_DC_CREDENTIAL = "X-Dc-Credential"; /** * X-Dc-Unit-Userヘッダ. * MasterTokenでのアクセス時に、このヘッダがある場合は、 * ヘッダ値で指定された任意のユニットユーザとして振る舞う。 */ public static final String X_DC_UNIT_USER = "X-Dc-Unit-User"; /** * Depthヘッダ. */ public static final String DEPTH = "Depth"; /** * X-HTTP-Method-Overrideヘッダ. */ public static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; /** * X-Overrideヘッダ. */ public static final String X_OVERRIDE = "X-Override"; /** * X-Forwarded-Protoヘッダ. */ public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; /** * X-Forwarded-Hostヘッダ. */ public static final String X_FORWARDED_HOST = "X-Forwarded-Host"; /** * X-Forwarded-Pathヘッダ. */ public static final String X_FORWARDED_PATH = "X-Forwarded-Path"; /** * X-Dc-Unit-Hostヘッダ. */ public static final String X_DC_UNIT_HOST = "X-Dc-Unit-Host"; /** * X-Dc-Versionヘッダ. */ public static final String X_DC_VERSION = "X-Dc-Version"; /** * X-Dc-Recursiveヘッダ. */ public static final String X_DC_RECURSIVE = "X-Dc-Recursive"; /** * X-Dc-RequestKeyヘッダ. */ public static final String X_DC_REQUESTKEY = "X-Dc-RequestKey"; /** * Access-Control-Allow-Originヘッダ. */ public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; /** * Access-Control-Allow-Headersヘッダ. */ public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; /** * Access-Control-Request-Headersヘッダ. */ public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; /** * Access-Control-Allow-Methodsヘッダ. */ public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; /** * Originヘッダ. */ public static final String ORIGIN = "Origin"; /** * Allowヘッダ. */ public static final String ALLOW = "Allow"; /** * Rangeヘッダ. */ public static final String RANGE = "Range"; /** * Accept-Rangeヘッダ. */ public static final String ACCEPT_RANGES = "Accept-Ranges"; /** * Content-Rangeヘッダ. */ public static final String CONTENT_RANGE = "Content-Range"; /** * 典型的なヘッダ値. */ public static class Value { /** * * */ public static final String ASTERISK = "*"; } } /** * Httpメソッドを定数としてこの下に定義します. */ public static class HttpMethod { /** * MERGE. */ public static final String MERGE = "MERGE"; /** * MKCOL. */ public static final String MKCOL = "MKCOL"; /** * PROPFIND. */ public static final String PROPFIND = "PROPFIND"; /** * PROPPATCH. */ public static final String PROPPATCH = "PROPPATCH"; /** * ACL. */ public static final String ACL = "ACL"; /** * COPY. */ public static final String COPY = "COPY"; /** * MOVE. */ public static final String MOVE = "MOVE"; /** * LOCK. */ public static final String LOCK = "LOCK"; /** * UNLOCK. */ public static final String UNLOCK = "UNLOCK"; } /** * サービスコレクションタイプを定数としてこの下に定義します. */ public static class XmlConst { /** * service. */ public static final String SERVICE = "service"; /** * odata. */ public static final String ODATA = "odata"; /** * urn:x-dc1:xmlns. */ public static final String NS_DC1 = "urn:x-dc1:xmlns"; /** * dc. */ public static final String NS_PREFIX_DC1 = "dc"; } /** * XMLのDOMノードを文字列に変換します. * @param node 文字列化したいノード * @return 変換結果の文字列 */ public static String nodeToString(final Node node) { StringWriter sw = new StringWriter(); try { Transformer t = TransformerFactory.newInstance().newTransformer(); t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); t.transform(new DOMSource(node), new StreamResult(sw)); } catch (TransformerException te) { throw new RuntimeException("nodeToString Transformer Exception", te); } return sw.toString(); } /** * プログラムリソース中のファイルをStringとして読み出します. * @param resPath リソースパス * @param encoding Encoding * @return 読み出した文字列 */ public static String readStringResource(final String resPath, final String encoding) { InputStream is = null; BufferedReader br = null; try { is = DcCoreUtils.class.getClassLoader().getResourceAsStream(resPath); br = new BufferedReader(new InputStreamReader(is, encoding)); String line = null; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line); } return sb.toString(); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } finally { try { if (is != null) { is.close(); } if (br != null) { br.close(); } } catch (IOException e) { throw new RuntimeException(e); } } } /** * Base64urlのエンコードを行う. * 厳密にはRFC4648参照。(といいたいが、少し表現があいまい。) --------------------------------------------------- * http://tools.ietf.org/html/rfc4648 --------------------------------------------------- 5. Base 64 Encoding with * URL and Filename Safe Alphabet The Base 64 encoding with an URL and filename safe alphabet has been used in [12]. * URLとファイル名で安全なアルファベットのベース64符号化は[12]で 使われました。 An alternative alphabet has been suggested that would use "~" as the * 63rd character. Since the "~" character has special meaning in some file system environments, the encoding * described in this section is recommended instead. The remaining unreserved URI character is ".", but some file * system environments do not permit multiple "." in a filename, thus making the "." character unattractive as well. * 63番目のアルファベットの代わりの文字として"~"を使うのが示唆され ました。"~"文字があるファイルシステム環境で特別な意味を持つので、 この章で記述された符号化はその代わりとして勧められます。残りの予 * 約なしのURI文字は"."ですが、あるファイルシステム環境で複数の"." は許されず、"."文字も魅力的でありません。 The pad character "=" is typically percent-encoded * when used in an URI [9], but if the data length is known implicitly, this can be avoided by skipping the padding; * see section 3.2. URI[9]で使われるとき、穴埋め文字"="は一般にパーセント符号化さ れますが、もしデータ長が暗黙のうちにわかるなら、穴埋めをスキップす * ることによってこれを避けれます;3.2章を見て下さい。 This encoding may be referred to as "base64url". This encoding should not be regarded * as the same as the "base64" encoding and should not be referred to as only "base64". Unless clarified otherwise, * "base64" refers to the base 64 in the previous section. この符号化は"base64url"と述べられるかもしれません。この符号化は * "base64"符号化と同じと見なされるべきではなくて、単純に「ベース6 4」と述べるべきではありません。他に明示されない限り、"base64"が 前章のベース64を意味します。 This encoding is * technically identical to the previous one, except for the 62:nd and 63:rd alphabet character, as indicated in * Table 2. この符号化は、62番目と63番目のアルファベット文字が表2で示さ れたものである以外は、技術的に前のものとまったく同じです。 Table 2: The "URL and Filename safe" Base * 64 Alphabet 表2:「URLとファイル名で安全な」ベース64アルファベット Value Encoding Value Encoding Value Encoding Value Encoding 0 A 17 R * 34 i 51 z 1 B 18 S 35 j 52 0 2 C 19 T 36 k 53 1 3 D 20 U 37 l 54 2 4 E 21 V 38 m 55 3 5 F 22 W 39 n 56 4 6 G 23 X * 40 o 57 5 7 H 24 Y 41 p 58 6 8 I 25 Z 42 q 59 7 9 J 26 a 43 r 60 8 10 K 27 b 44 s 61 9 11 L 28 c 45 t 62 - * (minus) 12 M 29 d 46 u 63 _ 13 N 30 e 47 v (underline) 14 O 31 f 48 w 15 P 32 g 49 x 16 Q 33 h 50 y (pad) = * @param in エンコードしたいbyte列 * @return エンコードされたあとの文字列 */ public static String encodeBase64Url(final byte[] in) { return Base64.encodeBase64URLSafeString(in); } /** * Base64urlのエンコードを行う. * @param inStr 入力ストリーム * @return 文字列 */ public static String encodeBase64Url(final InputStream inStr) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int l = 0; try { while ((l = inStr.read()) != -1) { baos.write(l); } } catch (IOException e) { throw new RuntimeException(e); } return Base64.encodeBase64URLSafeString(baos.toByteArray()); } /** * Base64urlのデコードを行う. * @param in デコードしたい文字列 * @return デコードされたbyte列 */ public static byte[] decodeBase64Url(final String in) { return Base64.decodeBase64(in); } static final int BITS_HEX_DIGIT = 4; static final int HEX_DIGIT_MASK = 0x0F; /** * バイト列を16進数の文字列に変換する. TODO さすがにこんなのどこかにライブラリありそうだけど. * @param input 入力バイト列 * @return 16進数文字列 */ public static String byteArray2HexString(final byte[] input) { StringBuffer buff = new StringBuffer(); int count = input.length; for (int i = 0; i < count; i++) { buff.append(Integer.toHexString((input[i] >> BITS_HEX_DIGIT) & HEX_DIGIT_MASK)); buff.append(Integer.toHexString(input[i] & HEX_DIGIT_MASK)); } return buff.toString(); } /** * URLエンコードのエンコードを行う. * @param in Urlエンコードしたい文字列 * @return Urlエンコードされた文字列 */ public static String encodeUrlComp(final String in) { try { return URLEncoder.encode(in, CharEncoding.UTF_8); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * URLエンコードのデコードを行う. * @param in Urlエンコードされた文字列 * @return 元の文字列 */ public static String decodeUrlComp(final String in) { try { return URLDecoder.decode(in, CharEncoding.UTF_8); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * InputStreamをすべて読み、String型で返す. * @param is InputStream * @return 文字列 */ public static String readInputStreamAsString(InputStream is) { InputStreamReader isr = null; BufferedReader reader = null; String bodyString = null; try { isr = new InputStreamReader(is, "UTF-8"); reader = new BufferedReader(isr); StringBuffer sb = new StringBuffer(); int chr; while ((chr = is.read()) != -1) { sb.append((char) chr); } bodyString = sb.toString(); } catch (IllegalStateException e) { log.info(e.getMessage()); } catch (IOException e) { log.info(e.getMessage()); } finally { try { if (reader != null) { reader.close(); } if (isr != null) { isr.close(); } if (is != null) { is.close(); } } catch (IOException e) { log.info(e.getMessage()); } } return bodyString; } private static final String AUTHZ_BASIC = "Basic "; /** * Authorizationヘッダの内容をBasic認証のものとしてパースする. * @param authzHeaderValue Authorizationヘッダの内容 * @return id, pwの2要素の文字列配列、またはパース失敗時はnull */ public static String[] parseBasicAuthzHeader(String authzHeaderValue) { if (authzHeaderValue == null || !authzHeaderValue.startsWith(AUTHZ_BASIC)) { return null; } try { // 認証スキーマ以外の部分を取得 byte[] bytes = DcCoreUtils.decodeBase64Url(authzHeaderValue.substring(AUTHZ_BASIC.length())); String rawStr = new String(bytes, CharEncoding.UTF_8); int pos = rawStr.indexOf(":"); // 認証トークンの値に「:」を含んでいない場合は認証エラーとする if (pos == -1) { return null; } String username = rawStr.substring(0, pos); String password = rawStr.substring(pos + 1); return new String[] {decodeUrlComp(username), decodeUrlComp(password) }; } catch (UnsupportedEncodingException e) { return null; } } /** * basic認証ヘッダを生成して返します. * @param id id * @param pw pw * @return basic認証のヘッダ */ public static String createBasicAuthzHeader(final String id, final String pw) { String line = encodeUrlComp(id) + ":" + encodeUrlComp(pw); try { return encodeBase64Url(line.getBytes(CharEncoding.UTF_8)); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * 任意のBaseUriをもつUriInfoオブジェクトを生成して返します. * @param uriInfo UriInfo * @param baseLevelsAbove BaseUriをRequestUriから何階層上にするか * @return UriInfo */ public static UriInfo createUriInfo(final UriInfo uriInfo, final int baseLevelsAbove) { DcUriInfo ret = new DcUriInfo(uriInfo, baseLevelsAbove, null); return ret; } /** * 任意のBaseUriをもつUriInfoオブジェクトを生成して返します. * @param uriInfo UriInfo * @param baseLevelsAbove BaseUriをRequestUriから何階層上にするか * @param add 追加パス情報 * @return UriInfo */ public static UriInfo createUriInfo(final UriInfo uriInfo, final int baseLevelsAbove, final String add) { DcUriInfo ret = new DcUriInfo(uriInfo, baseLevelsAbove, add); return ret; } static final int CHARS_PREFIX_ODATA_DATE = 7; static final int CHARS_SUFFIX_ODATA_DATE = 3; /** * ODataのDatetime JSONリテラル(Date(\/ ... \/) 形式)を解釈してDateオブジェクトに変換します. * @param odataDatetime ODataのDatetime JSONリテラル * @return Dateオブジェクト */ public static Date parseODataDatetime(final String odataDatetime) { String dateValue = odataDatetime .substring(CHARS_PREFIX_ODATA_DATE, odataDatetime.length() - CHARS_SUFFIX_ODATA_DATE); return new Date(Long.valueOf(dateValue)); } /** * OPTIONSメソッドに対する正常応答につかうResponseBuilderを作って返します. * @param allowedMethods 許可されるHTTPメソッド文字列. * @return ResponseBuilder */ public static ResponseBuilder responseBuilderForOptions(String... allowedMethods) { StringBuilder allowedMethodsBuilder = new StringBuilder(javax.ws.rs.HttpMethod.OPTIONS); if (allowedMethods != null && allowedMethods.length > 0) { allowedMethodsBuilder.append(", "); allowedMethodsBuilder.append(StringUtils.join(allowedMethods, ", ")); } return Response.ok().header(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, allowedMethodsBuilder.toString()) .header(HttpHeaders.ALLOW, allowedMethodsBuilder.toString()); } /** * 指定階層上のパスをBaseUri(ルート)とするUriInfoとして振る舞うUriInfoのWrapper. */ public static final class DcUriInfo implements UriInfo { UriBuilder baseUriBuilder; UriInfo core; /** * Constructor. * @param uriInfo UriInfo * @param baseLevelsAbove 何階層上のパスをルートとするか * @param add 追加パス情報 */ public DcUriInfo(final UriInfo uriInfo, final int baseLevelsAbove, final String add) { this.core = uriInfo; String reqUrl = uriInfo.getRequestUri().toASCIIString(); if (reqUrl.endsWith("/")) { reqUrl = reqUrl.substring(0, reqUrl.length() - 1); } String[] urlSplitted = reqUrl.split("/"); urlSplitted = (String[]) ArrayUtils.subarray(urlSplitted, 0, urlSplitted.length - baseLevelsAbove); reqUrl = StringUtils.join(urlSplitted, "/") + "/"; if (add != null && add.length() != 0) { reqUrl = reqUrl + add + "/"; } this.baseUriBuilder = UriBuilder.fromUri(reqUrl); } @Override public String getPath() { return this.getPath(true); } @Override public String getPath(final boolean decode) { String sReq = null; String sBas = null; if (decode) { sReq = this.getRequestUri().toString(); sBas = this.getBaseUri().toString(); } else { sReq = this.getRequestUri().toASCIIString(); sBas = this.getBaseUri().toASCIIString(); } return sReq.substring(sBas.length()); } @Override public List<PathSegment> getPathSegments() { return this.core.getPathSegments(); } @Override public List<PathSegment> getPathSegments(final boolean decode) { return this.core.getPathSegments(decode); } @Override public URI getRequestUri() { return this.core.getRequestUri(); } @Override public UriBuilder getRequestUriBuilder() { return this.core.getRequestUriBuilder(); } @Override public URI getAbsolutePath() { return this.core.getAbsolutePath(); } @Override public UriBuilder getAbsolutePathBuilder() { return this.core.getAbsolutePathBuilder(); } @Override public URI getBaseUri() { return this.baseUriBuilder.build(); } @Override public UriBuilder getBaseUriBuilder() { return this.baseUriBuilder; } @Override public MultivaluedMap<String, String> getPathParameters() { return this.core.getPathParameters(); } @Override public MultivaluedMap<String, String> getPathParameters(final boolean decode) { return this.core.getPathParameters(decode); } @Override public MultivaluedMap<String, String> getQueryParameters() { return this.core.getQueryParameters(); } @Override public MultivaluedMap<String, String> getQueryParameters(final boolean decode) { return this.core.getQueryParameters(decode); } @Override public List<String> getMatchedURIs() { return this.core.getMatchedURIs(); } @Override public List<String> getMatchedURIs(final boolean decode) { return this.core.getMatchedURIs(decode); } @Override public List<Object> getMatchedResources() { return this.core.getMatchedResources(); } } }