001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.lang3; 019 020import java.io.IOException; 021import java.util.Iterator; 022import java.util.StringJoiner; 023import java.util.function.Supplier; 024 025import org.apache.commons.lang3.exception.UncheckedException; 026import org.apache.commons.lang3.function.FailableBiConsumer; 027 028/** 029 * Joins an array or {@link Iterable} into an existing {@link Appendable} like a {@link StringBuilder}; with the goal for call sites to avoid creating 030 * intermediary Strings. This is like {@link String#join(CharSequence, CharSequence...)}, {@link String#join(CharSequence, Iterable)}, and {@link StringJoiner}. 031 * <p> 032 * Keep an instance in a (static) variable for efficient joining into an {@link Appendable} or {@link StringBuilder} without creating temporary Strings. 033 * </p> 034 * <p> 035 * Use the builder and instance methods to reuse the same kind of joining prefix, suffix, delimiter, and string conversion. 036 * </p> 037 * <p> 038 * For example: 039 * </p> 040 * 041 * <pre>{@code 042 * // A reuseable instance 043 * private static final AppendableJoiner<Object> JOINER = AppendableJoiner.builder() 044 * .setPrefix("[") 045 * .setSuffix("]") 046 * .setDelimiter(", ") 047 * .get(); 048 * } 049 * ... 050 * // Builds straight into a StringBuilder: 051 * StringBuilder sbuilder = new StringBuilder("1"); 052 * JOINER.join(sbuilder, "A", "B"); 053 * sbuilder.append("2"); 054 * JOINER.join(sbuilder, "C", "D"); 055 * sbuilder.append("3"); 056 * // Returns "1[A, B]2[C, D]3" 057 * return sbuilder.toString(); 058 * }</pre> 059 * <p> 060 * To provide a custom Object element to {@link CharSequence} converter, call {@link Builder#setElementAppender(FailableBiConsumer)}, for example: 061 * </p> 062 * 063 * <pre>{@code 064 * private static final AppendableJoiner<Item> JOINER = AppendableJoiner.builder() 065 * .setElementAppender(e -> (a, e) -> a.append(e.getFoo()) 066 * a.append(e.getBar()) 067 * a.append('!')) 068 * ... 069 * .get(); 070 * } 071 * }</pre> 072 * <p> 073 * This class is immutable and thread-safe. 074 * </p> 075 * 076 * @param <T> the type of elements to join. 077 * @see Appendable 078 * @see StringBuilder 079 * @see String#join(CharSequence, CharSequence...) 080 * @see String#join(CharSequence, Iterable) 081 * @see StringJoiner 082 * @since 3.15.0 083 */ 084public final class AppendableJoiner<T> { 085 086 /** 087 * Builds instances of {@link AppendableJoiner}. 088 * 089 * @param <T> the type of elements to join. 090 */ 091 public static final class Builder<T> implements Supplier<AppendableJoiner<T>> { 092 093 /** The sequence of characters to be used at the beginning. */ 094 private CharSequence prefix; 095 096 /** The sequence of characters to be used at the end. */ 097 private CharSequence suffix; 098 099 /** 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 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 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 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 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}