| // Copyright (C) 2012 The Android Open Source Project | |
| // | |
| // 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.google.gerrit.extensions.restapi; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.Closeable; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.nio.ByteBuffer; | |
| import java.nio.charset.CharacterCodingException; | |
| import java.nio.charset.Charset; | |
| import java.nio.charset.CodingErrorAction; | |
| import java.nio.charset.UnsupportedCharsetException; | |
| /** | |
| * Wrapper around a non-JSON result from a {@link RestView}. | |
| * | |
| * <p>Views may return this type to signal they want the server glue to write raw data to the | |
| * client, instead of attempting automatic conversion to JSON. The create form is overloaded to | |
| * handle plain text from a String, or binary data from a {@code byte[]} or {@code InputSteam}. | |
| */ | |
| public abstract class BinaryResult implements Closeable { | |
| /** Default MIME type for unknown binary data. */ | |
| static final String OCTET_STREAM = "application/octet-stream"; | |
| /** Produce a UTF-8 encoded result from a string. */ | |
| public static BinaryResult create(String data) { | |
| return new StringResult(data); | |
| } | |
| /** Produce an {@code application/octet-stream} result from a byte array. */ | |
| public static BinaryResult create(byte[] data) { | |
| return new Array(data); | |
| } | |
| /** | |
| * Produce an {@code application/octet-stream} of unknown length by copying the InputStream until | |
| * EOF. The server glue will automatically close this stream when copying is complete. | |
| */ | |
| public static BinaryResult create(InputStream data) { | |
| return new Stream(data); | |
| } | |
| private String contentType = OCTET_STREAM; | |
| private Charset characterEncoding; | |
| private long contentLength = -1; | |
| private boolean gzip = true; | |
| private boolean base64; | |
| private String attachmentName; | |
| /** Returns the MIME type of the result, for HTTP clients. */ | |
| public String getContentType() { | |
| Charset enc = getCharacterEncoding(); | |
| if (enc != null) { | |
| return contentType + "; charset=" + enc.name(); | |
| } | |
| return contentType; | |
| } | |
| /** Set the MIME type of the result, and return {@code this}. */ | |
| public BinaryResult setContentType(String contentType) { | |
| this.contentType = contentType != null ? contentType : OCTET_STREAM; | |
| return this; | |
| } | |
| /** Get the character encoding; null if not known. */ | |
| public Charset getCharacterEncoding() { | |
| return characterEncoding; | |
| } | |
| /** Set the character set used to encode text data and return {@code this}. */ | |
| public BinaryResult setCharacterEncoding(Charset encoding) { | |
| characterEncoding = encoding; | |
| return this; | |
| } | |
| /** Get the attachment file name; null if not set. */ | |
| public String getAttachmentName() { | |
| return attachmentName; | |
| } | |
| /** Set the attachment file name and return {@code this}. */ | |
| public BinaryResult setAttachmentName(String attachmentName) { | |
| this.attachmentName = attachmentName; | |
| return this; | |
| } | |
| /** Returns length in bytes of the result; -1 if not known. */ | |
| public long getContentLength() { | |
| return contentLength; | |
| } | |
| /** Set the content length of the result; -1 if not known. */ | |
| public BinaryResult setContentLength(long len) { | |
| this.contentLength = len; | |
| return this; | |
| } | |
| /** Returns true if this result can be gzip compressed to clients. */ | |
| public boolean canGzip() { | |
| return gzip; | |
| } | |
| /** Disable gzip compression for already compressed responses. */ | |
| public BinaryResult disableGzip() { | |
| this.gzip = false; | |
| return this; | |
| } | |
| /** Returns true if the result must be base64 encoded. */ | |
| public boolean isBase64() { | |
| return base64; | |
| } | |
| /** Wrap the binary data in base64 encoding. */ | |
| public BinaryResult base64() { | |
| base64 = true; | |
| return this; | |
| } | |
| /** | |
| * Write or copy the result onto the specified output stream. | |
| * | |
| * @param os stream to write result data onto. This stream will be closed by the caller after this | |
| * method returns. | |
| * @throws IOException if the data cannot be produced, or the OutputStream {@code os} throws any | |
| * IOException during a write or flush call. | |
| */ | |
| public abstract void writeTo(OutputStream os) throws IOException; | |
| /** | |
| * Return a copy of the result as a String. | |
| * | |
| * <p>The default version of this method copies the result into a temporary byte array and then | |
| * tries to decode it using the configured encoding. | |
| * | |
| * @return string version of the result. | |
| * @throws IOException if the data cannot be produced or could not be decoded to a String. | |
| */ | |
| public String asString() throws IOException { | |
| long len = getContentLength(); | |
| ByteArrayOutputStream buf; | |
| if (0 < len) { | |
| buf = new ByteArrayOutputStream((int) len); | |
| } else { | |
| buf = new ByteArrayOutputStream(); | |
| } | |
| writeTo(buf); | |
| return decode(buf.toByteArray(), getCharacterEncoding()); | |
| } | |
| /** Close the result and release any resources it holds. */ | |
| @Override | |
| public void close() throws IOException {} | |
| @Override | |
| public String toString() { | |
| if (getContentLength() >= 0) { | |
| return String.format( | |
| "BinaryResult[Content-Type: %s, Content-Length: %d]", | |
| getContentType(), getContentLength()); | |
| } | |
| return String.format( | |
| "BinaryResult[Content-Type: %s, Content-Length: unknown]", getContentType()); | |
| } | |
| private static String decode(byte[] data, Charset enc) { | |
| try { | |
| Charset cs = enc != null ? enc : UTF_8; | |
| return cs.newDecoder() | |
| .onMalformedInput(CodingErrorAction.REPORT) | |
| .onUnmappableCharacter(CodingErrorAction.REPORT) | |
| .decode(ByteBuffer.wrap(data)) | |
| .toString(); | |
| } catch (UnsupportedCharsetException | CharacterCodingException e) { | |
| // Fallback to ISO-8850-1 style encoding. | |
| StringBuilder r = new StringBuilder(data.length); | |
| for (byte b : data) { | |
| r.append((char) (b & 0xff)); | |
| } | |
| return r.toString(); | |
| } | |
| } | |
| private static class Array extends BinaryResult { | |
| private final byte[] data; | |
| Array(byte[] data) { | |
| this.data = data; | |
| setContentLength(data.length); | |
| } | |
| @Override | |
| public void writeTo(OutputStream os) throws IOException { | |
| os.write(data); | |
| } | |
| @Override | |
| public String asString() { | |
| return decode(data, getCharacterEncoding()); | |
| } | |
| } | |
| private static class StringResult extends Array { | |
| private final String str; | |
| StringResult(String str) { | |
| super(str.getBytes(UTF_8)); | |
| setContentType("text/plain"); | |
| setCharacterEncoding(UTF_8); | |
| this.str = str; | |
| } | |
| @Override | |
| public String asString() { | |
| return str; | |
| } | |
| } | |
| private static class Stream extends BinaryResult { | |
| private final InputStream src; | |
| Stream(InputStream src) { | |
| this.src = src; | |
| } | |
| @Override | |
| public void writeTo(OutputStream dst) throws IOException { | |
| byte[] tmp = new byte[4096]; | |
| int n; | |
| while (0 < (n = src.read(tmp))) { | |
| dst.write(tmp, 0, n); | |
| } | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| src.close(); | |
| } | |
| } | |
| } |