1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.lang3;
19
20 import java.io.IOException;
21 import java.util.Iterator;
22 import java.util.StringJoiner;
23 import java.util.function.Supplier;
24
25 import org.apache.commons.lang3.exception.UncheckedException;
26 import org.apache.commons.lang3.function.FailableBiConsumer;
27
28 /**
29 * Joins an array or {@link Iterable} into an existing {@link Appendable} like a {@link StringBuilder}; with the goal for call sites to avoid creating
30 * intermediary Strings. This is like {@link String#join(CharSequence, CharSequence...)}, {@link String#join(CharSequence, Iterable)}, and {@link StringJoiner}.
31 * <p>
32 * Keep an instance in a (static) variable for efficient joining into an {@link Appendable} or {@link StringBuilder} without creating temporary Strings.
33 * </p>
34 * <p>
35 * Use the builder and instance methods to reuse the same kind of joining prefix, suffix, delimiter, and string conversion.
36 * </p>
37 * <p>
38 * For example:
39 * </p>
40 *
41 * <pre>{@code
42 * // A reuseable instance
43 * private static final AppendableJoiner<Object> JOINER = AppendableJoiner.builder()
44 * .setPrefix("[")
45 * .setSuffix("]")
46 * .setDelimiter(", ")
47 * .get();
48 * }
49 * ...
50 * // Builds straight into a StringBuilder:
51 * StringBuilder sbuilder = new StringBuilder("1");
52 * JOINER.join(sbuilder, "A", "B");
53 * sbuilder.append("2");
54 * JOINER.join(sbuilder, "C", "D");
55 * sbuilder.append("3");
56 * // Returns "1[A, B]2[C, D]3"
57 * return sbuilder.toString();
58 * }</pre>
59 * <p>
60 * To provide a custom Object element to {@link CharSequence} converter, call {@link Builder#setElementAppender(FailableBiConsumer)}, for example:
61 * </p>
62 *
63 * <pre>{@code
64 * private static final AppendableJoiner<Item> JOINER = AppendableJoiner.builder()
65 * .setElementAppender(e -> (a, e) -> a.append(e.getFoo())
66 * a.append(e.getBar())
67 * a.append('!'))
68 * ...
69 * .get();
70 * }
71 * }</pre>
72 * <p>
73 * This class is immutable and thread-safe.
74 * </p>
75 *
76 * @param <T> the type of elements to join.
77 * @see Appendable
78 * @see StringBuilder
79 * @see String#join(CharSequence, CharSequence...)
80 * @see String#join(CharSequence, Iterable)
81 * @see StringJoiner
82 * @since 3.15.0
83 */
84 public final class AppendableJoiner<T> {
85
86 /**
87 * Builds instances of {@link AppendableJoiner}.
88 *
89 * @param <T> the type of elements to join.
90 */
91 public static final class Builder<T> implements Supplier<AppendableJoiner<T>> {
92
93 /** The sequence of characters to be used at the beginning. */
94 private CharSequence prefix;
95
96 /** The sequence of characters to be used at the end. */
97 private CharSequence suffix;
98
99 /** The delimiter that separates each element. */
100 private CharSequence delimiter;
101
102 /** The consumer used to render each element of type {@code T} onto an {@link Appendable}. */
103 private FailableBiConsumer<Appendable, T, IOException> appender;
104
105 /**
106 * Constructs a new instance.
107 */
108 Builder() {
109 // empty
110 }
111
112 /**
113 * Gets a new instance of {@link AppendableJoiner}.
114 */
115 @Override
116 public AppendableJoiner<T> get() {
117 return new AppendableJoiner<>(prefix, suffix, delimiter, appender);
118 }
119
120 /**
121 * Sets the delimiter that separates each element.
122 *
123 * @param delimiter The delimiter that separates each element.
124 * @return {@code this} instance.
125 */
126 public Builder<T> setDelimiter(final CharSequence delimiter) {
127 this.delimiter = delimiter;
128 return this;
129 }
130
131 /**
132 * Sets the consumer used to render each element of type {@code T} onto an {@link Appendable}.
133 *
134 * @param appender The consumer used to render each element of type {@code T} onto an {@link Appendable}.
135 * @return {@code this} instance.
136 */
137 public Builder<T> setElementAppender(final FailableBiConsumer<Appendable, T, IOException> appender) {
138 this.appender = appender;
139 return this;
140 }
141
142 /**
143 * Sets the sequence of characters to be used at the beginning.
144 *
145 * @param prefix The sequence of characters to be used at the beginning.
146 * @return {@code this} instance.
147 */
148 public Builder<T> setPrefix(final CharSequence prefix) {
149 this.prefix = prefix;
150 return this;
151 }
152
153 /**
154 * Sets the sequence of characters to be used at the end.
155 *
156 * @param suffix The sequence of characters to be used at the end.
157 * @return {@code this} instance.
158 */
159 public Builder<T> setSuffix(final CharSequence suffix) {
160 this.suffix = suffix;
161 return this;
162 }
163
164 }
165
166 /**
167 * Creates a new builder.
168 *
169 * @param <T> The type of elements.
170 * @return a new builder.
171 */
172 public static <T> Builder<T> builder() {
173 return new Builder<>();
174 }
175
176 /** Could be public in the future, in some form. */
177 @SafeVarargs
178 static <A extends Appendable, T> A joinA(final A appendable, final CharSequence prefix, final CharSequence suffix, final CharSequence delimiter,
179 final FailableBiConsumer<Appendable, T, IOException> appender, final T... elements) throws IOException {
180 return joinArray(appendable, prefix, suffix, delimiter, appender, elements);
181 }
182
183 private static <A extends Appendable, T> A joinArray(final A appendable, final CharSequence prefix, final CharSequence suffix, final CharSequence delimiter,
184 final FailableBiConsumer<Appendable, T, IOException> appender, final T[] elements) throws IOException {
185 appendable.append(prefix);
186 if (elements != null) {
187 if (elements.length > 0) {
188 appender.accept(appendable, elements[0]);
189 }
190 for (int i = 1; i < elements.length; i++) {
191 appendable.append(delimiter);
192 appender.accept(appendable, elements[i]);
193 }
194 }
195 appendable.append(suffix);
196 return appendable;
197 }
198
199 /** Could be public in the future, in some form. */
200 static <T> StringBuilder joinI(final StringBuilder stringBuilder, final CharSequence prefix, final CharSequence suffix, final CharSequence delimiter,
201 final FailableBiConsumer<Appendable, T, IOException> appender, final Iterable<T> elements) {
202 try {
203 return joinIterable(stringBuilder, prefix, suffix, delimiter, appender, elements);
204 } catch (final IOException e) {
205 // Cannot happen with a StringBuilder.
206 throw new UncheckedException(e);
207 }
208 }
209
210 private static <A extends Appendable, T> A joinIterable(final A appendable, final CharSequence prefix, final CharSequence suffix,
211 final CharSequence delimiter, final FailableBiConsumer<Appendable, T, IOException> appender, final Iterable<T> elements) throws IOException {
212 appendable.append(prefix);
213 if (elements != null) {
214 final Iterator<T> iterator = elements.iterator();
215 if (iterator.hasNext()) {
216 appender.accept(appendable, iterator.next());
217 }
218 while (iterator.hasNext()) {
219 appendable.append(delimiter);
220 appender.accept(appendable, iterator.next());
221 }
222 }
223 appendable.append(suffix);
224 return appendable;
225 }
226
227 /** Could be public in the future, in some form. */
228 @SafeVarargs
229 static <T> StringBuilder joinSB(final StringBuilder stringBuilder, final CharSequence prefix, final CharSequence suffix, final CharSequence delimiter,
230 final FailableBiConsumer<Appendable, T, IOException> appender, final T... elements) {
231 try {
232 return joinArray(stringBuilder, prefix, suffix, delimiter, appender, elements);
233 } catch (final IOException e) {
234 // Cannot happen with a StringBuilder.
235 throw new UncheckedException(e);
236 }
237 }
238
239 private static CharSequence nonNull(final CharSequence value) {
240 return value != null ? value : StringUtils.EMPTY;
241 }
242
243 /** The sequence of characters to be used at the beginning. */
244 private final CharSequence prefix;
245
246 /** The sequence of characters to be used at the end. */
247 private final CharSequence suffix;
248
249 /** The delimiter that separates each element. */
250 private final CharSequence delimiter;
251
252 private final FailableBiConsumer<Appendable, T, IOException> appender;
253
254 /**
255 * Constructs a new instance.
256 */
257 private AppendableJoiner(final CharSequence prefix, final CharSequence suffix, final CharSequence delimiter,
258 final FailableBiConsumer<Appendable, T, IOException> appender) {
259 this.prefix = nonNull(prefix);
260 this.suffix = nonNull(suffix);
261 this.delimiter = nonNull(delimiter);
262 this.appender = appender != null ? appender : (a, e) -> a.append(String.valueOf(e));
263 }
264
265 /**
266 * Joins stringified objects from the given Iterable into a StringBuilder.
267 *
268 * @param stringBuilder The target.
269 * @param elements The source.
270 * @return The given StringBuilder.
271 */
272 public StringBuilder join(final StringBuilder stringBuilder, final Iterable<T> elements) {
273 return joinI(stringBuilder, prefix, suffix, delimiter, appender, elements);
274 }
275
276 /**
277 * Joins stringified objects from the given array into a StringBuilder.
278 *
279 * @param stringBuilder The target.
280 * @param elements The source.
281 * @return the given target StringBuilder.
282 */
283 public StringBuilder join(final StringBuilder stringBuilder, @SuppressWarnings("unchecked") final T... elements) {
284 return joinSB(stringBuilder, prefix, suffix, delimiter, appender, elements);
285 }
286
287 /**
288 * Joins stringified objects from the given Iterable into an Appendable.
289 *
290 * @param <A> the Appendable type.
291 * @param appendable The target.
292 * @param elements The source.
293 * @return The given StringBuilder.
294 * @throws IOException If an I/O error occurs
295 */
296 public <A extends Appendable> A joinA(final A appendable, final Iterable<T> elements) throws IOException {
297 return joinIterable(appendable, prefix, suffix, delimiter, appender, elements);
298 }
299
300 /**
301 * Joins stringified objects from the given array into an Appendable.
302 *
303 * @param <A> the Appendable type.
304 * @param appendable The target.
305 * @param elements The source.
306 * @return The given StringBuilder.
307 * @throws IOException If an I/O error occurs
308 */
309 public <A extends Appendable> A joinA(final A appendable, @SuppressWarnings("unchecked") final T... elements) throws IOException {
310 return joinA(appendable, prefix, suffix, delimiter, appender, elements);
311 }
312
313 }