/* * JBoss, Home of Professional Open Source. * Copyright 2014, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.controller.client.impl; import java.io.DataInput; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.jboss.as.controller.client.OperationResponse; import org.jboss.as.protocol.StreamUtils; import org.jboss.as.protocol.mgmt.AbstractManagementRequest; import org.jboss.as.protocol.mgmt.ActiveOperation; import org.jboss.as.protocol.mgmt.FlushableDataOutput; import org.jboss.as.protocol.mgmt.ManagementChannelAssociation; import org.jboss.as.protocol.mgmt.ManagementRequest; import org.jboss.as.protocol.mgmt.ManagementRequestContext; import org.jboss.as.protocol.mgmt.ProtocolUtils; import org.jboss.dmr.ModelNode; /** * An {@link org.jboss.as.controller.client.OperationResponse} that proxies back to a remote server * to read any attached response streams. * * @author Brian Stansberry (c) 2014 Red Hat Inc. */ public class OperationResponseProxy implements OperationResponse { private final ModelNode responseNode; private final Map<String, StreamEntry> proxiedStreams; private OperationResponseProxy(final ModelNode responseNode, final ManagementChannelAssociation channelAssociation, final int batchId, final ModelNode streamHeader) { this.responseNode = responseNode; int size = streamHeader.asInt(); proxiedStreams = new LinkedHashMap<String, StreamEntry>(size); for (int i = 0; i < size; i++) { ModelNode headerElement = streamHeader.get(i); final String uuid = headerElement.require("uuid").asString(); final String mimeType = headerElement.require("mime-type").asString(); proxiedStreams.put(uuid, new ProxiedInputStream(uuid, mimeType, channelAssociation, batchId, i)); } } public static OperationResponseProxy create(final ModelNode responseNode, final ManagementChannelAssociation channelAssociation, final int batchId, final ModelNode streamHeader) { return new OperationResponseProxy(responseNode, channelAssociation, batchId, streamHeader); } @Override public ModelNode getResponseNode() { return responseNode; } @Override public List<StreamEntry> getInputStreams() { List<StreamEntry> result = new ArrayList<StreamEntry>(); result.addAll(proxiedStreams.values()); return Collections.unmodifiableList(result); } @Override public StreamEntry getInputStream(String uuid) { return proxiedStreams.get(uuid); } @Override public void close() throws IOException { for (StreamEntry se : proxiedStreams.values()) { se.getStream().close(); } } private static class ProxiedInputStream extends InputStream implements StreamEntry { static final int BUFFER_SIZE = 8192; private final String uuid; private final String mimeType; private final int index; private final int batchId; private final Pipe pipe; private final ManagementChannelAssociation channelAssociation; private volatile boolean remoteClosed; private boolean remoteRead; private volatile Exception error; ProxiedInputStream(final String uuid, final String mimeType, final ManagementChannelAssociation channelAssociation, final int batchId, final int index) { this.uuid = uuid; this.mimeType = mimeType; this.channelAssociation = channelAssociation; this.batchId = batchId; this.index = index; pipe = new Pipe(BUFFER_SIZE); } @Override public int read() throws IOException { if (available() < 1) { readRemote(); } return pipe.getIn().read(); } @Override public int read(byte[] b, int off, int len) throws IOException { if (available() < len) { readRemote(); } return pipe.getIn().read(b, off, len); } @Override public void close() throws IOException { IOException ex = null; try { closeRemote(); } catch (IOException e) { ex = e; } try { pipe.getOut().close(); } catch (IOException e) { if (ex == null) { ex = e; } } try { pipe.getIn().close(); } catch (IOException e) { if (ex == null) { ex = e; } } if (ex != null) { throw ex; } } @Override public int available() throws IOException { return pipe.getIn().available(); } private void readRemote() throws IOException { readInputStream(); throwIfError(); } private synchronized void readInputStream() { if (remoteRead || remoteClosed) { return; } final OutputStream os = pipe.getOut(); // Execute the async request final ManagementRequest<Void, Void> getISRequest = new AbstractManagementRequest<Void, Void>() { @Override public byte getOperationType() { return ModelControllerProtocol.GET_CHUNKED_INPUTSTREAM_REQUEST; } @Override protected void sendRequest(ActiveOperation.ResultHandler<Void> resultHandler, ManagementRequestContext<Void> context, FlushableDataOutput output) throws IOException { output.write(ModelControllerProtocol.PARAM_OPERATION); output.writeInt(batchId); output.write(ModelControllerProtocol.PARAM_INPUTSTREAM_INDEX); output.writeInt(index); } @Override public void handleRequest(DataInput input, ActiveOperation.ResultHandler<Void> resultHandler, ManagementRequestContext<Void> context) throws IOException { try { // Loop reading chunk until we get an end message IOException pipeWriteException = null; for (;;) { byte header = input.readByte(); if (header == ModelControllerProtocol.PARAM_END) { remoteClosed = true; break; } ProtocolUtils.expectHeader(header, ModelControllerProtocol.PARAM_INPUTSTREAM_LENGTH); int size = input.readInt(); ProtocolUtils.expectHeader(input, ModelControllerProtocol.PARAM_INPUTSTREAM_CONTENTS); final byte[] buffer = new byte[BUFFER_SIZE]; int totalRead = 0; while (totalRead < size) { int len = Math.min(size - totalRead, buffer.length); input.readFully(buffer, 0, len); if (pipeWriteException == null) { try { os.write(buffer, 0, len); } catch (IOException e) { // The ProxiedInputStream must have been closed // From now on we just read and discard the bytes pipeWriteException = e; } } // else just read the bytes off the network and discard totalRead += len; } } os.close(); if (pipeWriteException != null) { throw pipeWriteException; } resultHandler.done(null); } catch (IOException e) { shutdown(e); resultHandler.failed(e); throw e; } } }; try { channelAssociation.executeRequest(getISRequest, null); remoteRead = true; } catch (IOException e) { shutdown(e); } } private void closeRemote() throws IOException { if (!remoteClosed) { final ManagementRequest<Void, Void> closeRequest = new AbstractManagementRequest<Void, Void>() { @Override public byte getOperationType() { return ModelControllerProtocol.CLOSE_INPUTSTREAM_REQUEST; } @Override protected void sendRequest(ActiveOperation.ResultHandler<Void> resultHandler, ManagementRequestContext<Void> context, FlushableDataOutput output) throws IOException { output.write(ModelControllerProtocol.PARAM_OPERATION); output.writeInt(batchId); output.write(ModelControllerProtocol.PARAM_INPUTSTREAM_INDEX); output.writeInt(index); } @Override public void handleRequest(DataInput input, ActiveOperation.ResultHandler<Void> resultHandler, ManagementRequestContext<Void> context) throws IOException { remoteClosed = true; } }; channelAssociation.executeRequest(closeRequest, null); } } private void throwIfError() throws IOException { if (error != null) { if (error instanceof IOException) { throw (IOException) error; } throw new IOException(error); } } private void shutdown(Exception error) { StreamUtils.safeClose(this); this.error = error; } @Override public String getUUID() { return uuid; } @Override public String getMimeType() { return mimeType; } @Override public InputStream getStream() { return this; } } }