org.glassfish.admin.payload.ZipPayloadImpl Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.admin.payload;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.glassfish.api.admin.Payload;
/**
* Implementation of Payload based on representing each payload Part as a
* ZipEntry in a zip stream in the request or response stream.
*
* Note that when retrieving Parts from a Payload that is implemented this way the calling
* program must consume each Part's data - by invoking the Part's getInputStream()
* method and exhausting the stream - before advancing to the next Part
* (using the Iterator returned by the parts() method). This is because all
* Parts share the same single ZipInputStream from the payload. To save space
* and improve performance this implementation does not store each Part's
* contents internally. That's why the calling program must consume each Part's content
* before moving on to the next.
*
* Each ZipEntry supports "extra" data. This implementation stores the Part properties
* in this extra data. Further, each Part can have a different content-type and
* the Properties object stored in the extra data also records the content-type
* for the Part.
*
* @author tjquinn
*/
class ZipPayloadImpl extends PayloadImpl {
/**
* requests and responses using the zip payload implementation should have
* the Content-Type set to application/zip.
*/
private static final String PAYLOAD_IMPL_CONTENT_TYPE =
"application/zip";
/**
* Zip implementation of the Outbound Payload.
*/
static class Outbound extends PayloadImpl.Outbound {
private void prepareEntry(final Payload.Part part, final ZipOutputStream zos) throws IOException {
ZipEntry entry = new ZipEntry(part.getName());
Extra extra = new Extra(part.getContentType(), part.getProperties());
entry.setExtra(extra.toBytes());
zos.putNextEntry(entry);
}
@Override
public void writePartsTo(OutputStream os) throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(os)) {
for (Payload.Part part : getParts()) {
prepareEntry(part, zos);
part.copy(zos);
zos.closeEntry();
}
}
}
private Outbound() {
}
public static Outbound newInstance() {
return new Outbound();
}
@Override
public String getComplexContentType() {
return PAYLOAD_IMPL_CONTENT_TYPE;
}
}
/**
* Zip implementation of the Inbound Payload.
*
* A single ZipInputStream provides the data for each ZipEntry
* in the Payload. So the returned InputStream for each Part is
* actually a wrapper stream around the single shared ZipInputStream.
* A given Part's input stream becomes invalid once the caller reads to
* the end of that ZipEntry OR if the caller moves on to the next Part
* (using the iterator) before having reached the end of the stream.
*
* For this to work, the inbound payload prefetches the next entry from
* the ZipInputStream and stores it internally. The implementation of
* the Iterator uses this prefetched next entry to decide if there is a
* next result from the iterator, for example.
*
* The next entry is prefetched when:
*
* - the inbound implementation is first
* created,
*
- the caller invokes the iterator's hasNext() method when
* the next entry has not already been prefetched (such as if the caller
* did not read the Part's input stream at all or invoked the iterator's
* hasNext() method before reading to the end of the part's input stream), and
*
- the caller exhausts the input stream from a Part.
*
*/
static class Inbound extends PayloadImpl.Inbound {
private final ZipInputStream zis;
private ZipEntry nextEntry = null;
private boolean isNextEntryPrefetched = false;
private Inbound(final InputStream is) throws IOException {
zis = new ZipInputStream(new BufferedInputStream(is));
prefetchNextEntry();
}
private void invalidateCurrentWrapperStream() {
}
private void prefetchNextEntry() throws IOException {
invalidateCurrentWrapperStream();
nextEntry = zis.getNextEntry();
isNextEntryPrefetched = true;
}
private void recordZipEntryEOF() throws IOException {
invalidateCurrentWrapperStream();
prefetchNextEntry();
}
private void recordZipEntryNonEOF() {
isNextEntryPrefetched = false;
}
/**
* Wrapper stream around the ZipInputStream. This stream becomes
* invalid once the caller reads to the end of this entry's
* data in the ZipInputStream or once the caller moves on to the
* next entry using the iterator returned by the payload's parts()
* method.
*/
private static class ZipEntryInputStream extends InputStream {
private final ZipInputStream zis;
private final Inbound inboundPayload;
private boolean isValid = true;
private ZipEntryInputStream(final Inbound inboundPayload) {
this.zis = inboundPayload.zis;
this.inboundPayload = inboundPayload;
}
private void invalidate() {
isValid = false;
}
private void checkValid() {
if ( ! isValid) {
throw new IllegalStateException();
}
}
@Override
public int read() throws IOException {
checkValid();
int result = zis.read();
if (result == -1) {
inboundPayload.recordZipEntryEOF();
} else {
inboundPayload.recordZipEntryNonEOF();
}
return result;
}
@Override
public int read(byte[] b) throws IOException {
checkValid();
int result = zis.read(b);
if (result == -1 ) {
inboundPayload.recordZipEntryEOF();
} else {
inboundPayload.recordZipEntryNonEOF();
}
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
checkValid();
int result = zis.read(b, off, len);
if (result == -1) {
inboundPayload.recordZipEntryEOF();
} else {
inboundPayload.recordZipEntryNonEOF();
}
return result;
}
@Override
public void close() throws IOException {
invalidate();
}
}
/**
* Returns a new Zip implementation of the Inbound Payload.
* @param payloadContentType content type for the payload
* @param is InputStream from which to read the payload
* @return Payload.Inbound containing the data from the specified input stream;
* null if the payloadContentType is not what this zip-based
* implementation can handle
* @throws java.io.IOException
*/
public static Inbound newInstance(final String payloadContentType, final InputStream is) throws IOException {
return new Inbound(is);
}
/**
* Does this Inbound Payload implementation support the given content type?
* @return true if the content type is supported
*/
public static boolean supportsContentType(final String contentType) {
return PAYLOAD_IMPL_CONTENT_TYPE.equalsIgnoreCase(contentType);
}
@Override
public Iterator parts() {
return new Iterator<>() {
@Override
public boolean hasNext() {
if ( ! isNextEntryPrefetched) {
try {
prefetchNextEntry();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
return (nextEntry != null);
}
@Override
public Payload.Part next() {
final Extra extra = new Extra(nextEntry.getExtra());
final Payload.Part part = new ZipPayloadImpl.Part(
nextEntry.getName(),
extra.getContentType(),
extra.getProperties(),
Inbound.this);
isNextEntryPrefetched = false;
return part;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
/**
* Zip-based implementation of the Part interface.
*/
static class Part extends PayloadImpl.Part {
private final Inbound inboundPayload;
private Part(
final String name,
final String contentType,
final Properties props,
final Inbound inboundPayload) {
super(contentType, name, props);
this.inboundPayload = inboundPayload;
}
@Override
public InputStream getInputStream() {
return new Inbound.ZipEntryInputStream(inboundPayload);
}
}
/**
* Abstraction of our use of the ZipEntry's "extra" data.
*
* We use the ZipEntry extra data to hold Properties file-formatted
* data holding the data request information plus the content type for
* the Part.
*
* The "normal" properties and the content type are exposed separately to
* the rest of the ipmlementation but we use a single Properties dump to
* represent both. So before exposing the Properties object we remove
* the content-type entry.
*/
private static class Extra {
private static final String CONTENT_TYPE_NAME = "Content-Type";
private String contentType;
private Properties props;
private Extra(final byte[] extra) {
try {
props = new Properties();
ByteArrayInputStream bais = new ByteArrayInputStream(extra);
props.load(bais);
contentType = props.getProperty(CONTENT_TYPE_NAME);
props.remove(CONTENT_TYPE_NAME);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private Extra(final String contentType, final Properties props) {
this.contentType = contentType;
this.props = props;
}
private byte[] toBytes() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Properties fullProps = new Properties();
if (props != null) {
fullProps.putAll(props);
}
fullProps.setProperty(CONTENT_TYPE_NAME, contentType);
try {
fullProps.store(baos, null);
return baos.toByteArray();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private String getContentType() {
return contentType;
}
private Properties getProperties() {
return props;
}
}
}