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 }