View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.mina.filter.compression;
21  
22  import java.io.IOException;
23  
24  import org.apache.mina.core.buffer.IoBuffer;
25  import org.apache.mina.core.filterchain.IoFilter;
26  import org.apache.mina.core.filterchain.IoFilterChain;
27  import org.apache.mina.core.session.AttributeKey;
28  import org.apache.mina.core.session.IoSession;
29  import org.apache.mina.core.write.WriteRequest;
30  import org.apache.mina.filter.util.WriteRequestFilter;
31  
32  /**
33   * An {@link IoFilter} which compresses all data using
34   * <a href="http://www.jcraft.com/jzlib/">JZlib</a>.
35   * Support for the LZW (DLCZ) algorithm is also planned.
36   * <p>
37   * This filter only supports compression using the <tt>PARTIAL FLUSH</tt> method,
38   * since that is the only method useful when doing stream level compression.
39   * <p>
40   * This filter supports compression/decompression of the input and output
41   * channels selectively.  It can also be enabled/disabled on the fly.
42   * <p>
43   * This filter does not discard the zlib objects, keeping them around for the
44   * entire life of the filter.  This is because the zlib dictionary needs to
45   * be built up over time, which is used during compression and decompression.
46   * Over time, as repetitive data is sent over the wire, the compression efficiency
47   * steadily increases.
48   * <p>
49   * Note that the zlib header is written only once. It is not necessary that
50   * the data received after processing by this filter may not be complete due
51   * to packet fragmentation.
52   * <p>
53   * It goes without saying that the other end of this stream should also have a
54   * compatible compressor/decompressor using the same algorithm.
55   *
56   * @author The Apache MINA Project (dev@mina.apache.org)
57   * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (jeu, 26 jun 2008) $
58   */
59  public class CompressionFilter extends WriteRequestFilter {
60      /**
61       * Max compression level.  Will give the highest compression ratio, but
62       * will also take more cpu time and is the slowest.
63       */
64      public static final int COMPRESSION_MAX = Zlib.COMPRESSION_MAX;
65  
66      /**
67       * Provides the best speed at the price of a low compression ratio.
68       */
69      public static final int COMPRESSION_MIN = Zlib.COMPRESSION_MIN;
70  
71      /**
72       * No compression done on the data.
73       */
74      public static final int COMPRESSION_NONE = Zlib.COMPRESSION_NONE;
75  
76      /**
77       * The default compression level used. Provides the best balance
78       * between speed and compression
79       */
80      public static final int COMPRESSION_DEFAULT = Zlib.COMPRESSION_DEFAULT;
81  
82      /**
83       * A session attribute that stores the {@link Zlib} object used for compression.
84       */
85      private final AttributeKey DEFLATER = new AttributeKey(getClass(), "deflater");
86  
87      /**
88       * A session attribute that stores the {@link Zlib} object used for decompression.
89       */
90      private final AttributeKey INFLATER = new AttributeKey(getClass(), "inflater");
91  
92      /**
93       * A flag that allows you to disable compression once.
94       */
95      public static final AttributeKey DISABLE_COMPRESSION_ONCE = new AttributeKey(CompressionFilter.class, "disableOnce"); 
96  
97      private boolean compressInbound = true;
98  
99      private boolean compressOutbound = true;
100 
101     private int compressionLevel;
102 
103     /**
104      * Creates a new instance which compresses outboud data and decompresses
105      * inbound data with default compression level.
106      */
107     public CompressionFilter() {
108         this(true, true, COMPRESSION_DEFAULT);
109     }
110 
111     /**
112      * Creates a new instance which compresses outboud data and decompresses
113      * inbound data with the specified <tt>compressionLevel</tt>.
114      *
115      * @param compressionLevel the level of compression to be used. Must
116      *                         be one of {@link #COMPRESSION_DEFAULT},
117      *                         {@link #COMPRESSION_MAX},
118      *                         {@link #COMPRESSION_MIN}, and
119      *                         {@link #COMPRESSION_NONE}.
120      */
121     public CompressionFilter(final int compressionLevel) {
122         this(true, true, compressionLevel);
123     }
124 
125     /**
126      * Creates a new instance.
127      *
128      * @param compressInbound <tt>true</tt> if data read is to be decompressed
129      * @param compressOutbound <tt>true</tt> if data written is to be compressed
130      * @param compressionLevel the level of compression to be used. Must
131      *                         be one of {@link #COMPRESSION_DEFAULT},
132      *                         {@link #COMPRESSION_MAX},
133      *                         {@link #COMPRESSION_MIN}, and
134      *                         {@link #COMPRESSION_NONE}.
135      */
136     public CompressionFilter(final boolean compressInbound,
137             final boolean compressOutbound, final int compressionLevel) {
138         this.compressionLevel = compressionLevel;
139         this.compressInbound = compressInbound;
140         this.compressOutbound = compressOutbound;
141     }
142 
143     @Override
144     public void messageReceived(NextFilter nextFilter, IoSession session,
145             Object message) throws Exception {
146         if (!compressInbound || !(message instanceof IoBuffer)) {
147             nextFilter.messageReceived(session, message);
148             return;
149         }
150 
151         Zlib inflater = (Zlib) session.getAttribute(INFLATER);
152         if (inflater == null) {
153             throw new IllegalStateException();
154         }
155 
156         IoBuffer inBuffer = (IoBuffer) message;
157         IoBuffer outBuffer = inflater.inflate(inBuffer);
158         nextFilter.messageReceived(session, outBuffer);
159     }
160 
161     /*
162      * @see org.apache.mina.core.IoFilter#filterWrite(org.apache.mina.core.IoFilter.NextFilter, org.apache.mina.core.IoSession, org.apache.mina.core.IoFilter.WriteRequest)
163      */
164     @Override
165     protected Object doFilterWrite(
166             NextFilter nextFilter, IoSession session,
167             WriteRequest writeRequest) throws IOException {
168         if (!compressOutbound) {
169             return null;
170         }
171 
172         if (session.containsAttribute(DISABLE_COMPRESSION_ONCE)) {
173             // Remove the marker attribute because it is temporary.
174             session.removeAttribute(DISABLE_COMPRESSION_ONCE);
175             return null;
176         }
177 
178         Zlib deflater = (Zlib) session.getAttribute(DEFLATER);
179         if (deflater == null) {
180             throw new IllegalStateException();
181         }
182 
183         IoBuffer inBuffer = (IoBuffer) writeRequest.getMessage();
184         if (!inBuffer.hasRemaining()) {
185             // Ignore empty buffers
186             return null;
187         } else {
188             return deflater.deflate(inBuffer);
189         }
190     }
191 
192     @Override
193     public void onPreAdd(IoFilterChain parent, String name,
194             NextFilter nextFilter) throws Exception {
195         if (parent.contains(CompressionFilter.class)) {
196             throw new IllegalStateException(
197                     "Only one " + CompressionFilter.class + " is permitted.");
198         }
199 
200         Zlib deflater = new Zlib(compressionLevel, Zlib.MODE_DEFLATER);
201         Zlib inflater = new Zlib(compressionLevel, Zlib.MODE_INFLATER);
202 
203         IoSession session = parent.getSession();
204 
205         session.setAttribute(DEFLATER, deflater);
206         session.setAttribute(INFLATER, inflater);
207     }
208 
209     /**
210      * Returns <tt>true</tt> if incoming data is being compressed.
211      */
212     public boolean isCompressInbound() {
213         return compressInbound;
214     }
215 
216     /**
217      * Sets if incoming data has to be compressed.
218      */
219     public void setCompressInbound(boolean compressInbound) {
220         this.compressInbound = compressInbound;
221     }
222 
223     /**
224      * Returns <tt>true</tt> if the filter is compressing data being written.
225      */
226     public boolean isCompressOutbound() {
227         return compressOutbound;
228     }
229 
230     /**
231      * Set if outgoing data has to be compressed.
232      */
233     public void setCompressOutbound(boolean compressOutbound) {
234         this.compressOutbound = compressOutbound;
235     }
236 
237     @Override
238     public void onPostRemove(IoFilterChain parent, String name,
239             NextFilter nextFilter) throws Exception {
240         super.onPostRemove(parent, name, nextFilter);
241         IoSession session = parent.getSession();
242         if (session == null) {
243             return;
244         }
245 
246         Zlib inflater = (Zlib) session.getAttribute(INFLATER);
247         Zlib deflater = (Zlib) session.getAttribute(DEFLATER);
248         if (deflater != null) {
249             deflater.cleanUp();
250         }
251 
252         if (inflater != null) {
253             inflater.cleanUp();
254         }
255     }
256 }