View Javadoc
1   /*
2   Copyright (c) 2007 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.common.util;
18  
19  import java.util.Iterator;
20  import java.io.IOException;
21  
22  /**
23   * Wrapper for an Appendable which adds the ability for objects to append
24   * themselves directly to the given Appendable instead of creating
25   * intermediate CharSequence objects (Strings, etc).  This can make appends of
26   * deep object hierarchies much more efficient.  An object can take advantage
27   * of this facility by implementing the Appendee interface.
28   * <p>
29   * All methods will use the {@link Appendee#appendTo} method if passed an
30   * Appendee.
31   * <p>
32   * Additionally, this wrapper adds two convenience methods:
33   * <ul>
34   * <li>{@link #append(Object)} - a method to append Objects by calling
35   *     String.valueOf(obj) on the object and appending the result to the
36   *     AppendableExt (unless the object is an Appendee or CharSequence)</li>
37   * <li>{@link #append(Iterable,Object)} - a method to append an iteration
38   *     (collection, etc) of objects separated by a delimiter</li>
39   * </ul>
40   * This class acts like the
41   * {@link java.util.Formatter}/{@link java.util.Formattable} facility without
42   * the extra overhead of parsing the format strings.  If complicated formatting
43   * is needed for the generated strings, Formatter should be used instead.
44   * <p>
45   * Examples:
46   * </p>
47   * <pre>
48   *
49   * //
50   * // *Without* using this interface.
51   * //
52   * public class Foo {
53   *   private Bar b;
54   *   public String toString() {
55   *     return "Foo " + b;
56   *   }
57   * }
58   * public class Bar {
59   *   public String toString() {
60   *     return "Bar";
61   *   }
62   * }
63   *
64   * Foo f = new Foo();
65   * StringBuilder sb = new StringBuilder();
66   *
67   * // this will involve copying multiple strings before actual append!!!
68   * sb.append(f);
69   *
70   *
71   * //
72   * // *With* using this interface.
73   * //
74   * public class Foo implements Appendee {
75   *   private Bar b;
76   *   public void appendTo(AppendableExt app) throws IOException {
77   *     app.append("Foo").append(b);
78   *   }
79   * }
80   * public class Bar implements Appendee {
81   *   public void appendTo(AppendableExt app) throws IOException {
82   *     app.append("Bar");
83   *   }
84   * }
85   *
86   * Foo f = new Foo();
87   * AppendableExt ae = new AppendableExt(new StringBuilder());
88   *
89   * // this will involve no extra copies, both strings ("Foo", "Bar") will be
90   * // written directly to the Appender
91   * ae.append(f);
92   *
93   * </pre>
94   *
95   * @author James Ahlborn
96   */
97  public class AppendableExt implements Appendable
98  {
99    /**
100    * The actual Appendable getting the chars
101    */
102   private final Appendable _baseApp;
103 
104   /**
105    * Working appendable which may be changed during certain operations
106    */
107   private Appendable _app;
108 
109   /**
110    * Custom context for the appendable
111    */
112   private Object _context;
113 
114   /**
115    * Initialize a new AppendableExt based on the given Appendable.
116    *
117    * @param app initial underlying Appendable
118    */
119   public AppendableExt(Appendable app) {
120     this(app, null);
121   }
122 
123   /**
124    * Initialize a new AppendableExt based on the given Appendable and
125    * context.
126    *
127    * @param app initial underlying Appendable
128    * @param context initial append context
129    */
130   public AppendableExt(Appendable app, Object context) {
131     _baseApp = app;
132     _app = app;
133     _context = context;
134   }
135 
136   @Override
137   public AppendableExt append(char c)
138     throws IOException
139   {
140     _app.append(c);
141     return this;
142   }
143 
144   /**
145    * {@inheritDoc}
146    * <p>
147    * Note that if the given CharSequence is also an Appendee, it will be
148    * handled as an Appendee instead.
149    */
150   @Override
151   public AppendableExt append(CharSequence s)
152     throws IOException
153   {
154     if(s instanceof Appendee) {
155       return this.append((Appendee)s);
156     }
157 
158     _app.append(s);
159     return this;
160   }
161 
162   /**
163    * {@inheritDoc}
164    * <p>
165    * Note that if the given CharSequence is also an Appendee, it will be
166    * handled as an Appendee instead (but the range will still be respected).
167    */
168   @Override
169   public AppendableExt append(CharSequence s, int start, int end)
170     throws IOException
171   {
172     if(start == end) {
173       // empty range, don't bother doing any work
174       return this;
175     }
176 
177     if(s instanceof Appendee) {
178       // we want to allow the appendee to append directly, but we need to
179       // restrict the range of output.  so, we will put a SubRangeAppendable
180       // into place for this call which will filter out the unneeded
181       // characters while still allowing for direct appending
182       Appendable oldApp = _app;
183       _app = new SubRangeAppendable(oldApp, start, end);
184       try {
185         return this.append((Appendee)s);
186       } finally {
187         _app = oldApp;
188       }
189     }
190 
191     _app.append(s, start, end);
192     return this;
193   }
194 
195   /**
196    * Will call the appendTo() method on the given object.
197    *
198    * @param a object to append to this AppendableExt
199    * @return this AppendableExt object
200    * @throws IOException if the append fails
201    */
202   public AppendableExt append(Appendee a)
203     throws IOException
204   {
205     a.appendTo(this);
206     return this;
207   }
208 
209   /**
210    * Will call append(String.valueOf(o)) with the given object (unless the
211    * object is an Appendee or CharSequence in which case it will be passed to
212    * the appropriate method).
213    *
214    * @param o object to append to this AppendableExt
215    * @return this AppendableExt object
216    * @throws IOException if the append fails
217    */
218   public AppendableExt append(Object o)
219     throws IOException
220   {
221     if(o instanceof Appendee) {
222       return this.append((Appendee)o);
223     }
224     if(o instanceof CharSequence) {
225       return this.append((CharSequence)o);
226     }
227 
228     _app.append(String.valueOf(o));
229     return this;
230   }
231 
232   /**
233    * Will iterate the given Iterable and append each object separated by the
234    * given delimiter.
235    *
236    * @param iable an Iterable object
237    * @param delimiter delimiter to append between each object in the iable
238    * @return this AppendableExt object
239    * @throws IOException if the append fails
240    */
241   public AppendableExt append(Iterable<?> iable, Object delimiter)
242     throws IOException
243   {
244     for(Iterator<?> iter = iable.iterator(); iter.hasNext(); ) {
245       append(iter.next());
246       if(iter.hasNext()) {
247         append(delimiter);
248       }
249     }
250     return this;
251   }
252 
253   /**
254    * Get the underlying Appendable.
255    *
256    * @return the underlying Appendable
257    */
258   public Appendable getAppendable() { return _baseApp; }
259 
260   /**
261    * Get the result of calling toString() on the underlying Appendable.
262    *
263    * @return the result of calling toString() on the underlying Appendable.
264    */
265   @Override
266   public String toString() {
267     return _baseApp.toString();
268   }
269 
270   /**
271    * @return the current "context" as set through {@link #setContext}, if any.
272    */
273   public Object getContext() {
274     return _context;
275   }
276 
277   /**
278    * Sets the "context" that will be returned from subsequent
279    * {@link #getContext} calls.  Useful for Appendee implementations that
280    * want to change the behavior of nested, context-aware Appendee objects.
281    *
282    * @param newContext the new context for any subsequent append calls
283    */
284   public void setContext(Object newContext) {
285     _context = newContext;
286   }
287 
288   /**
289    * Appendable which delegates actual appending to another appendable, but
290    * restricts the passed through characters based on a given range.
291    */
292   private static class SubRangeAppendable implements Appendable
293   {
294     private final Appendable _delegate;
295     private final int _start;
296     private final int _end;
297     private int _pos;
298 
299     private SubRangeAppendable(Appendable delegate,
300                                int start, int end) {
301       _delegate = delegate;
302       _start = start;
303       _end = end;
304     }
305 
306     @Override
307     public Appendable append(char c)
308       throws IOException
309     {
310       if((_pos >= _start) && (_pos < _end)) {
311         _delegate.append(c);
312       }
313       ++_pos;
314       return this;
315     }
316 
317     @Override
318     public Appendable append(CharSequence csq)
319       throws IOException
320     {
321       int len = csq.length();
322       if((_pos >= _start) && ((_end - _pos) >= len)) {
323         // special case entire incoming range being valid
324         _delegate.append(csq);
325         _pos += len;
326       } else {
327         // handle range manipulation in common method
328         append(csq, 0, len);
329       }
330       return this;
331     }
332 
333     @Override
334     public Appendable append(CharSequence csq, int start, int end)
335       throws IOException
336     {
337       if((start < 0) || (end < 0) || (start > end) || (end > csq.length())) {
338         throw new IndexOutOfBoundsException("invalid start " + start +
339                                             " or end " + end + ", length " +
340                                             csq.length());
341       }
342 
343       if(_pos >= _end) {
344         // already past our target range, skip everything else
345         return this;
346       }
347 
348       // skip any leading chars in the incoming range that are not within our
349       // target range
350       if(_pos < _start) {
351         int skip = Math.min((end - start), (_start - _pos));
352         start += skip;
353         _pos += skip;
354       }
355 
356       if(start >= end) {
357         // no chars left in incoming range
358         return this;
359       }
360 
361       // shrink incoming range if fewer chars are needed in target range
362       int len = Math.min((end - start), (_end - _pos));
363       end = start + len;
364 
365       // finally pass the adjusted range to the delegate
366       _delegate.append(csq, start, end);
367       _pos += len;
368 
369       return this;
370     }
371 
372   }
373 
374 }