Line data Source code
1 : // Copyright (C) 2012 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.extensions.restapi; 16 : 17 : import static java.nio.charset.StandardCharsets.UTF_8; 18 : 19 : import java.io.ByteArrayOutputStream; 20 : import java.io.Closeable; 21 : import java.io.IOException; 22 : import java.io.InputStream; 23 : import java.io.OutputStream; 24 : import java.nio.ByteBuffer; 25 : import java.nio.charset.CharacterCodingException; 26 : import java.nio.charset.Charset; 27 : import java.nio.charset.CodingErrorAction; 28 : import java.nio.charset.UnsupportedCharsetException; 29 : 30 : /** 31 : * Wrapper around a non-JSON result from a {@link RestView}. 32 : * 33 : * <p>Views may return this type to signal they want the server glue to write raw data to the 34 : * client, instead of attempting automatic conversion to JSON. The create form is overloaded to 35 : * handle plain text from a String, or binary data from a {@code byte[]} or {@code InputSteam}. 36 : */ 37 38 : public abstract class BinaryResult implements Closeable { 38 : /** Default MIME type for unknown binary data. */ 39 : static final String OCTET_STREAM = "application/octet-stream"; 40 : 41 : /** Produce a UTF-8 encoded result from a string. */ 42 : public static BinaryResult create(String data) { 43 25 : return new StringResult(data); 44 : } 45 : 46 : /** Produce an {@code application/octet-stream} result from a byte array. */ 47 : public static BinaryResult create(byte[] data) { 48 18 : return new Array(data); 49 : } 50 : 51 : /** 52 : * Produce an {@code application/octet-stream} of unknown length by copying the InputStream until 53 : * EOF. The server glue will automatically close this stream when copying is complete. 54 : */ 55 : public static BinaryResult create(InputStream data) { 56 0 : return new Stream(data); 57 : } 58 : 59 38 : private String contentType = OCTET_STREAM; 60 : private Charset characterEncoding; 61 38 : private long contentLength = -1; 62 38 : private boolean gzip = true; 63 : private boolean base64; 64 : private String attachmentName; 65 : 66 : /** Returns the MIME type of the result, for HTTP clients. */ 67 : public String getContentType() { 68 27 : Charset enc = getCharacterEncoding(); 69 27 : if (enc != null) { 70 27 : return contentType + "; charset=" + enc.name(); 71 : } 72 19 : return contentType; 73 : } 74 : 75 : /** Set the MIME type of the result, and return {@code this}. */ 76 : public BinaryResult setContentType(String contentType) { 77 38 : this.contentType = contentType != null ? contentType : OCTET_STREAM; 78 38 : return this; 79 : } 80 : 81 : /** Get the character encoding; null if not known. */ 82 : public Charset getCharacterEncoding() { 83 35 : return characterEncoding; 84 : } 85 : 86 : /** Set the character set used to encode text data and return {@code this}. */ 87 : public BinaryResult setCharacterEncoding(Charset encoding) { 88 30 : characterEncoding = encoding; 89 30 : return this; 90 : } 91 : 92 : /** Get the attachment file name; null if not set. */ 93 : public String getAttachmentName() { 94 27 : return attachmentName; 95 : } 96 : 97 : /** Set the attachment file name and return {@code this}. */ 98 : public BinaryResult setAttachmentName(String attachmentName) { 99 4 : this.attachmentName = attachmentName; 100 4 : return this; 101 : } 102 : 103 : /** Returns length in bytes of the result; -1 if not known. */ 104 : public long getContentLength() { 105 27 : return contentLength; 106 : } 107 : 108 : /** Set the content length of the result; -1 if not known. */ 109 : public BinaryResult setContentLength(long len) { 110 38 : this.contentLength = len; 111 38 : return this; 112 : } 113 : 114 : /** Returns true if this result can be gzip compressed to clients. */ 115 : public boolean canGzip() { 116 27 : return gzip; 117 : } 118 : 119 : /** Disable gzip compression for already compressed responses. */ 120 : public BinaryResult disableGzip() { 121 3 : this.gzip = false; 122 3 : return this; 123 : } 124 : 125 : /** Returns true if the result must be base64 encoded. */ 126 : public boolean isBase64() { 127 27 : return base64; 128 : } 129 : 130 : /** Wrap the binary data in base64 encoding. */ 131 : public BinaryResult base64() { 132 20 : base64 = true; 133 20 : return this; 134 : } 135 : 136 : /** 137 : * Write or copy the result onto the specified output stream. 138 : * 139 : * @param os stream to write result data onto. This stream will be closed by the caller after this 140 : * method returns. 141 : * @throws IOException if the data cannot be produced, or the OutputStream {@code os} throws any 142 : * IOException during a write or flush call. 143 : */ 144 : public abstract void writeTo(OutputStream os) throws IOException; 145 : 146 : /** 147 : * Return a copy of the result as a String. 148 : * 149 : * <p>The default version of this method copies the result into a temporary byte array and then 150 : * tries to decode it using the configured encoding. 151 : * 152 : * @return string version of the result. 153 : * @throws IOException if the data cannot be produced or could not be decoded to a String. 154 : */ 155 : public String asString() throws IOException { 156 3 : long len = getContentLength(); 157 : ByteArrayOutputStream buf; 158 3 : if (0 < len) { 159 0 : buf = new ByteArrayOutputStream((int) len); 160 : } else { 161 3 : buf = new ByteArrayOutputStream(); 162 : } 163 3 : writeTo(buf); 164 3 : return decode(buf.toByteArray(), getCharacterEncoding()); 165 : } 166 : 167 : /** Close the result and release any resources it holds. */ 168 : @Override 169 32 : public void close() throws IOException {} 170 : 171 : @Override 172 : public String toString() { 173 0 : if (getContentLength() >= 0) { 174 0 : return String.format( 175 : "BinaryResult[Content-Type: %s, Content-Length: %d]", 176 0 : getContentType(), getContentLength()); 177 : } 178 0 : return String.format( 179 0 : "BinaryResult[Content-Type: %s, Content-Length: unknown]", getContentType()); 180 : } 181 : 182 : private static String decode(byte[] data, Charset enc) { 183 : try { 184 16 : Charset cs = enc != null ? enc : UTF_8; 185 16 : return cs.newDecoder() 186 16 : .onMalformedInput(CodingErrorAction.REPORT) 187 16 : .onUnmappableCharacter(CodingErrorAction.REPORT) 188 16 : .decode(ByteBuffer.wrap(data)) 189 16 : .toString(); 190 0 : } catch (UnsupportedCharsetException | CharacterCodingException e) { 191 : // Fallback to ISO-8850-1 style encoding. 192 0 : StringBuilder r = new StringBuilder(data.length); 193 0 : for (byte b : data) { 194 0 : r.append((char) (b & 0xff)); 195 : } 196 0 : return r.toString(); 197 : } 198 : } 199 : 200 : private static class Array extends BinaryResult { 201 : private final byte[] data; 202 : 203 33 : Array(byte[] data) { 204 33 : this.data = data; 205 33 : setContentLength(data.length); 206 33 : } 207 : 208 : @Override 209 : public void writeTo(OutputStream os) throws IOException { 210 25 : os.write(data); 211 25 : } 212 : 213 : @Override 214 : public String asString() { 215 15 : return decode(data, getCharacterEncoding()); 216 : } 217 : } 218 : 219 : private static class StringResult extends Array { 220 : private final String str; 221 : 222 : StringResult(String str) { 223 25 : super(str.getBytes(UTF_8)); 224 25 : setContentType("text/plain"); 225 25 : setCharacterEncoding(UTF_8); 226 25 : this.str = str; 227 25 : } 228 : 229 : @Override 230 : public String asString() { 231 5 : return str; 232 : } 233 : } 234 : 235 : private static class Stream extends BinaryResult { 236 : private final InputStream src; 237 : 238 0 : Stream(InputStream src) { 239 0 : this.src = src; 240 0 : } 241 : 242 : @Override 243 : public void writeTo(OutputStream dst) throws IOException { 244 0 : byte[] tmp = new byte[4096]; 245 : int n; 246 0 : while (0 < (n = src.read(tmp))) { 247 0 : dst.write(tmp, 0, n); 248 : } 249 0 : } 250 : 251 : @Override 252 : public void close() throws IOException { 253 0 : src.close(); 254 0 : } 255 : } 256 : }