/*************************************************************************
* Copyright 2009-2013 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
*
* This file may incorporate work covered under the following copyright
* and permission notice:
*
* Software License Agreement (BSD License)
*
* Copyright (c) 2008, Regents of the University of California
* All rights reserved.
*
* Redistribution and use of this software in source and binary forms,
* with or without modification, are permitted provided that the
* following conditions are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE
* THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL,
* COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE,
* AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING
* IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA,
* SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY,
* WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION,
* REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO
* IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT
* NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS.
************************************************************************/
package com.eucalyptus.ws.handlers;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.MissingFormatArgumentException;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.log4j.Logger;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.DefaultHttpChunk;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMessage;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import com.eucalyptus.binding.Binding;
import com.eucalyptus.binding.BindingException;
import com.eucalyptus.binding.BindingManager;
import com.eucalyptus.binding.HoldMe;
import com.eucalyptus.component.ComponentId;
import com.eucalyptus.component.annotation.ComponentPart;
import com.eucalyptus.context.Context;
import com.eucalyptus.context.Contexts;
import com.eucalyptus.http.MappingHttpRequest;
import com.eucalyptus.http.MappingHttpResponse;
import com.eucalyptus.records.Logs;
import com.eucalyptus.util.UnsafeByteArrayOutputStream;
import com.eucalyptus.ws.EucalyptusWebServiceException;
import com.eucalyptus.ws.protocol.RequiredQueryParams;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import edu.ucsb.eucalyptus.msgs.BaseMessage;
import edu.ucsb.eucalyptus.msgs.EucalyptusErrorMessageType;
import edu.ucsb.eucalyptus.msgs.ExceptionResponseType;
public abstract class RestfulMarshallingHandler extends MessageStackHandler {
private static Logger LOG = Logger.getLogger( RestfulMarshallingHandler.class );
private String namespace;
private final String namespacePattern;
private Binding defaultBinding = BindingManager.getDefaultBinding( );
private String defaultVersion;
private Binding binding;
private final Class<? extends ComponentId> component;
public RestfulMarshallingHandler( String namespacePattern ) {
this.namespacePattern = namespacePattern;
this.component = getClass().getAnnotation( ComponentPart.class ) == null
? null :
getClass().getAnnotation( ComponentPart.class ).value();
try {
this.setNamespace( String.format( namespacePattern ) );
} catch ( MissingFormatArgumentException ex ) {}
}
public RestfulMarshallingHandler( final String namespacePattern, final String defaultVersion ) {
this( namespacePattern );
final String defaultBindingNamespace = String.format( namespacePattern, defaultVersion );
this.defaultBinding = BindingManager.getBinding( BindingManager.sanitizeNamespace( defaultBindingNamespace ), component );
this.defaultVersion = defaultVersion;
}
@Override
public void incomingMessage( MessageEvent event ) throws Exception {
if ( event.getMessage( ) instanceof MappingHttpRequest ) {
MappingHttpRequest httpRequest = ( MappingHttpRequest ) event.getMessage( );
String bindingVersion = httpRequest.getParameters( ).remove( RequiredQueryParams.Version.toString( ) );
if ( Strings.isNullOrEmpty( bindingVersion ) && defaultVersion != null) {
this.setNamespaceVersion( defaultVersion );
} else if ( bindingVersion != null && bindingVersion.matches( "\\d\\d\\d\\d-\\d\\d-\\d\\d" ) ) {
this.setNamespaceVersion( bindingVersion );
} else {
this.setNamespace( BindingManager.defaultBindingName() );
}
try {
BaseMessage msg = ( BaseMessage ) this.bind( httpRequest );
httpRequest.setMessage( msg );
} catch ( Exception e ) {
if ( !( e instanceof BindingException ) ) {
e = new BindingException( e );
}
throw e;
}
}
}
protected void setNamespace( String namespace ) {
this.namespace = namespace;
this.binding = BindingManager.getBinding( this.namespace, component );
}
protected String getNamespaceForVersion( String bindingVersion ) {
return String.format( this.namespacePattern, bindingVersion );
}
private void setNamespaceVersion( String bindingVersion ) {
String newNs = null;
try {
newNs = String.format( this.namespacePattern );
} catch ( MissingFormatArgumentException e ) {
newNs = String.format( this.namespacePattern, bindingVersion );
}
this.setNamespace( newNs );
}
public abstract Object bind( MappingHttpRequest httpRequest ) throws Exception;
@Override
public void outgoingMessage( ChannelHandlerContext ctx, MessageEvent event ) throws Exception {
if ( event.getMessage( ) instanceof MappingHttpResponse ) {
MappingHttpResponse httpResponse = ( MappingHttpResponse ) event.getMessage( );
UnsafeByteArrayOutputStream byteOut = new UnsafeByteArrayOutputStream( 8192 );
HoldMe.canHas.lock( );
try {
if ( httpResponse.getMessage( ) == null ) {
/** TODO:GRZE: doing nothing here may be needed for streaming? double check... **/
// String response = Binding.createRestFault( this.requestType.get( ctx.getChannel( ) ), "Recieved an response from the service which has no content.", "" );
// byteOut.write( response.getBytes( ) );
// httpResponse.setStatus( HttpResponseStatus.INTERNAL_SERVER_ERROR );
} else if ( httpResponse.getMessage( ) instanceof EucalyptusErrorMessageType ) {
EucalyptusErrorMessageType errMsg = ( EucalyptusErrorMessageType ) httpResponse.getMessage( );
byteOut.write( Binding.createRestFault( errMsg.getSource( ), errMsg.getMessage( ), errMsg.getCorrelationId( ) ).getBytes( ) );
httpResponse.setStatus( HttpResponseStatus.BAD_REQUEST );
} else if ( httpResponse.getMessage( ) instanceof ExceptionResponseType ) {//handle error case specially
ExceptionResponseType msg = ( ExceptionResponseType ) httpResponse.getMessage( );
String detail = msg.getError( );
if( msg.getException( ) != null ) {
Logs.extreme( ).debug( msg, msg.getException( ) );
}
if ( msg.getException() instanceof EucalyptusWebServiceException ) {
detail = msg.getCorrelationId( );
}
String response = Binding.createRestFault( msg.getRequestType( ), msg.getMessage( ), detail );
byteOut.write( response.getBytes( ) );
httpResponse.setStatus( msg.getHttpStatus( ) );
} else {//actually try to bind response
final Object message = httpResponse.getMessage( );
try {//use request binding
this.binding.toStream( byteOut, message, getNamespaceOverride( message, null ) );
} catch ( BindingException ex ) {
Logs.extreme( ).error( ex, ex );
byteOut.reset();
try {//use default binding with request namespace
getDefaultBinding( ).toStream( byteOut, message, getNamespaceOverride( message, this.namespace ) );
} catch ( BindingException ex1 ) {//use default binding
byteOut.reset();
BindingManager.getDefaultBinding( ).toStream( byteOut, message );
}
} catch ( Exception e ) {
LOG.debug( e );
Logs.exhaust( ).error( e, e );
throw e;
}
}
ChannelBuffer buffer = ChannelBuffers.wrappedBuffer( byteOut.getBuffer( ), 0, byteOut.getCount( ) );
httpResponse.addHeader( HttpHeaders.Names.CONTENT_LENGTH, String.valueOf( buffer.readableBytes( ) ) );
httpResponse.addHeader( HttpHeaders.Names.CONTENT_TYPE, "application/xml; charset=UTF-8" );
httpResponse.setContent( buffer );
} finally {
HoldMe.canHas.unlock( );
}
}
}
public static void streamResponse( final Object message ) {
final Context context = Contexts.lookup();
final Channel channel = context.getChannel();
final RestfulMarshallingHandler handler =
channel.getPipeline().get(RestfulMarshallingHandler.class);
final DefaultHttpResponse httpResponse =
new DefaultHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
final EncodingWrapper wrapper = getEncodingWrapper();
wrapper.writeHeaders( httpResponse );
try {
httpResponse.addHeader( HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED );
httpResponse.addHeader( HttpHeaders.Names.CONTENT_TYPE, "application/xml; charset=UTF-8" );
final OutputStream outputStream = wrapper.wrapOutput(
new BufferedOutputStream( new OutputStream() {
@Override
public void write( final int b ) throws IOException {
write( new byte[]{ (byte)b }, 0, 1 );
}
@Override
public void write( final byte[] b, final int off, final int len ) throws IOException {
final ChannelFuture future = Channels.write( channel,
new DefaultHttpChunk(ChannelBuffers.wrappedBuffer(b, off, len)));
if ( !future.awaitUninterruptibly().isSuccess() ) {
throw new IOException( future.getCause() );
}
}
@Override
public void flush() throws IOException {
}
} ) );
Channels.write( channel, httpResponse );
handler.binding.toStream( outputStream, message );
outputStream.close(); // the implementations used flush/complete on close
Channels.write( channel, HttpChunk.LAST_CHUNK );
} catch ( final Exception e ) {
LOG.error( "Error streaming response", e );
}
}
protected String getNamespaceOverride( @Nonnull final Object message,
@Nullable final String namespace ) {
return namespace;
}
/**
* Get the encoding wrapper to use for the request.
*
* This simple check for encoding support fails if a request specifies any
* "q" value (i.e. encoding preferences, or any other parameters or
* extension)
*/
static EncodingWrapper getEncodingWrapper() {
EncodingWrapper wrapper = null;
final MappingHttpRequest request = Contexts.lookup().getHttpRequest();
if ( request != null ) {
final String accept =
Objects.firstNonNull( request.getHeader( HttpHeaders.Names.ACCEPT_ENCODING ), "" ).toLowerCase();
if ( accept.matches( "[a-z, *_-]{1,1024}" ) ) {
final Iterable<String> encodings = Splitter.on(",").trimResults().omitEmptyStrings().split(accept);
if ( Iterables.contains( encodings, HttpHeaders.Values.DEFLATE ) ) {
wrapper = new DeflateEncodingWrapper();
} else if ( Iterables.contains( encodings, HttpHeaders.Values.GZIP ) ) {
wrapper = new GzipEncodingWrapper();
}
}
}
if ( wrapper == null ) {
wrapper = new EncodingWrapper();
}
return wrapper;
}
private static class EncodingWrapper {
void writeHeaders( @Nonnull final HttpMessage httpResponse ) {
}
@Nonnull
OutputStream wrapOutput( @Nonnull final OutputStream out ) throws IOException {
return out;
}
}
private static abstract class CompressingEncodingWrapper extends EncodingWrapper {
private final String encoding;
protected CompressingEncodingWrapper(final String encoding) {
this.encoding = encoding;
}
@Override
void writeHeaders( @Nonnull final HttpMessage httpResponse ) {
httpResponse.addHeader( HttpHeaders.Names.CONTENT_ENCODING, encoding );
}
@Nonnull
@Override
final OutputStream wrapOutput( @Nonnull final OutputStream out ) throws IOException {
return compress( out );
}
@Nonnull
abstract OutputStream compress( @Nonnull OutputStream out ) throws IOException;
}
private static final class DeflateEncodingWrapper extends CompressingEncodingWrapper {
DeflateEncodingWrapper() {
super( HttpHeaders.Values.DEFLATE );
}
@Nonnull
@Override
OutputStream compress( @Nonnull final OutputStream out ) {
return new DeflaterOutputStream( out );
}
}
private static final class GzipEncodingWrapper extends CompressingEncodingWrapper {
private GzipEncodingWrapper() {
super( HttpHeaders.Values.GZIP );
}
@Nonnull
@Override
OutputStream compress( @Nonnull final OutputStream out ) throws IOException {
return new GZIPOutputStream( out );
}
}
/**
* @return the namespace
*/
public String getNamespace( ) {
return this.namespace;
}
/**
* @return the binding
*/
public Binding getBinding( ) {
return this.binding;
}
public Binding getDefaultBinding( ) {
return this.defaultBinding;
}
}