1 /* 2 * Copyright (C) 2008 Google 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.google.common.base; 18 19 import com.google.common.annotations.GwtCompatible; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 22 import java.io.IOException; 23 import java.util.AbstractList; 24 import java.util.Arrays; 25 import java.util.Iterator; 26 import java.util.Map; 27 import java.util.Map.Entry; 28 29 import javax.annotation.Nullable; 30 31 /** 32 * An object which joins pieces of text (specified as an array, {@link 33 * Iterable}, varargs or even a {@link Map}) with a separator. It either 34 * appends the results to an {@link Appendable} or returns them as a {@link 35 * String}. Example: <pre> {@code 36 * 37 * Joiner joiner = Joiner.on("; ").skipNulls(); 38 * . . . 39 * return joiner.join("Harry", null, "Ron", "Hermione");}</pre> 40 * 41 * This returns the string {@code "Harry; Ron; Hermione"}. Note that all input 42 * elements are converted to strings using {@link Object#toString()} before 43 * being appended. 44 * 45 * <p>If neither {@link #skipNulls()} nor {@link #useForNull(String)} is 46 * specified, the joining methods will throw {@link NullPointerException} if any 47 * given element is null. 48 * 49 * @author Kevin Bourrillion 50 * @since 2010.01.04 <b>stable</b> (imported from Google Collections Library) 51 */ 52 @GwtCompatible public class Joiner { 53 /** 54 * Returns a joiner which automatically places {@code separator} between 55 * consecutive elements. 56 */ 57 public static Joiner on(String separator) { 58 return new Joiner(separator); 59 } 60 61 /** 62 * Returns a joiner which automatically places {@code separator} between 63 * consecutive elements. 64 */ 65 public static Joiner on(char separator) { 66 return new Joiner(String.valueOf(separator)); 67 } 68 69 private final String separator; 70 71 private Joiner(String separator) { 72 this.separator = checkNotNull(separator); 73 } 74 75 private Joiner(Joiner prototype) { 76 this.separator = prototype.separator; 77 } 78 79 /** 80 * Appends the string representation of each of {@code parts}, using the 81 * previously configured separator between each, to {@code appendable}. 82 */ 83 public <A extends Appendable> A appendTo(A appendable, Iterable<?> parts) 84 throws IOException { 85 checkNotNull(appendable); 86 Iterator<?> iterator = parts.iterator(); 87 if (iterator.hasNext()) { 88 appendable.append(toString(iterator.next())); 89 while (iterator.hasNext()) { 90 appendable.append(separator); 91 appendable.append(toString(iterator.next())); 92 } 93 } 94 return appendable; 95 } 96 97 /** 98 * Appends the string representation of each of {@code parts}, using the 99 * previously configured separator between each, to {@code appendable}. 100 */ 101 public final <A extends Appendable> A appendTo( 102 A appendable, Object[] parts) throws IOException { 103 return appendTo(appendable, Arrays.asList(parts)); 104 } 105 106 /** 107 * Appends to {@code appendable} the string representation of each of the 108 * remaining arguments. 109 */ 110 public final <A extends Appendable> A appendTo(A appendable, 111 @Nullable Object first, @Nullable Object second, Object... rest) 112 throws IOException { 113 return appendTo(appendable, iterable(first, second, rest)); 114 } 115 116 /** 117 * Appends the string representation of each of {@code parts}, using the 118 * previously configured separator between each, to {@code builder}. Identical 119 * to {@link #appendTo(Appendable, Iterable)}, except that it does not throw 120 * {@link IOException}. 121 */ 122 public final StringBuilder appendTo(StringBuilder builder, Iterable<?> parts) 123 { 124 try { 125 appendTo((Appendable) builder, parts); 126 } catch (IOException impossible) { 127 throw new AssertionError(impossible); 128 } 129 return builder; 130 } 131 132 /** 133 * Appends the string representation of each of {@code parts}, using the 134 * previously configured separator between each, to {@code builder}. Identical 135 * to {@link #appendTo(Appendable, Iterable)}, except that it does not throw 136 * {@link IOException}. 137 */ 138 public final StringBuilder appendTo(StringBuilder builder, Object[] parts) { 139 return appendTo(builder, Arrays.asList(parts)); 140 } 141 142 /** 143 * Appends to {@code builder} the string representation of each of the 144 * remaining arguments. Identical to {@link #appendTo(Appendable, Object, 145 * Object, Object[])}, except that it does not throw {@link IOException}. 146 */ 147 public final StringBuilder appendTo(StringBuilder builder, 148 @Nullable Object first, @Nullable Object second, Object... rest) { 149 return appendTo(builder, iterable(first, second, rest)); 150 } 151 152 /** 153 * Returns a string containing the string representation of each of {@code 154 * parts}, using the previously configured separator between each. 155 */ 156 public final String join(Iterable<?> parts) { 157 return appendTo(new StringBuilder(), parts).toString(); 158 } 159 160 /** 161 * Returns a string containing the string representation of each of {@code 162 * parts}, using the previously configured separator between each. 163 */ 164 public final String join(Object[] parts) { 165 return join(Arrays.asList(parts)); 166 } 167 168 /** 169 * Returns a string containing the string representation of each argument, 170 * using the previously configured separator between each. 171 */ 172 public final String join( 173 @Nullable Object first, @Nullable Object second, Object... rest) { 174 return join(iterable(first, second, rest)); 175 } 176 177 /** 178 * Returns a joiner with the same behavior as this one, except automatically 179 * substituting {@code nullText} for any provided null elements. 180 */ 181 public Joiner useForNull(final String nullText) { 182 checkNotNull(nullText); 183 return new Joiner(this) { 184 @Override CharSequence toString(Object part) { 185 return (part == null) ? nullText : Joiner.this.toString(part); 186 } 187 @Override public Joiner useForNull(String nullText) { 188 checkNotNull(nullText); // weird, just to satisfy NullPointerTester! 189 // TODO: fix that? 190 throw new UnsupportedOperationException("already specified useForNull"); 191 } 192 @Override public Joiner skipNulls() { 193 throw new UnsupportedOperationException("already specified useForNull"); 194 } 195 }; 196 } 197 198 /** 199 * Returns a joiner with the same behavior as this joiner, except 200 * automatically skipping over any provided null elements. 201 */ 202 public Joiner skipNulls() { 203 return new Joiner(this) { 204 @Override public <A extends Appendable> A appendTo( 205 A appendable, Iterable<?> parts) throws IOException { 206 checkNotNull(appendable, "appendable"); 207 checkNotNull(parts, "parts"); 208 Iterator<?> iterator = parts.iterator(); 209 while (iterator.hasNext()) { 210 Object part = iterator.next(); 211 if (part != null) { 212 appendable.append(Joiner.this.toString(part)); 213 break; 214 } 215 } 216 while (iterator.hasNext()) { 217 Object part = iterator.next(); 218 if (part != null) { 219 appendable.append(separator); 220 appendable.append(Joiner.this.toString(part)); 221 } 222 } 223 return appendable; 224 } 225 @Override public Joiner useForNull(String nullText) { 226 checkNotNull(nullText); // weird, just to satisfy NullPointerTester! 227 throw new UnsupportedOperationException("already specified skipNulls"); 228 } 229 @Override public MapJoiner withKeyValueSeparator(String kvs) { 230 checkNotNull(kvs); // weird, just to satisfy NullPointerTester! 231 throw new UnsupportedOperationException( 232 "can't use .skipNulls() with maps"); 233 } 234 }; 235 } 236 237 /** 238 * Returns a {@code MapJoiner} using the given key-value separator, and the 239 * same configuration as this {@code Joiner} otherwise. 240 */ 241 public MapJoiner withKeyValueSeparator(String keyValueSeparator) { 242 return new MapJoiner(this, checkNotNull(keyValueSeparator)); 243 } 244 245 /** 246 * An object that joins map entries in the same manner as {@code Joiner} joins 247 * iterables and arrays. 248 */ 249 public static class MapJoiner { 250 private Joiner joiner; 251 private String keyValueSeparator; 252 253 private MapJoiner(Joiner joiner, String keyValueSeparator) { 254 this.joiner = joiner; 255 this.keyValueSeparator = keyValueSeparator; 256 } 257 258 /** 259 * Appends the string representation of each entry of {@code map}, using the 260 * previously configured separator and key-value separator, to {@code 261 * appendable}. 262 */ 263 public <A extends Appendable> A appendTo(A appendable, Map<?, ?> map) 264 throws IOException { 265 checkNotNull(appendable); 266 Iterator<? extends Map.Entry<?, ?>> iterator = map.entrySet().iterator(); 267 if (iterator.hasNext()) { 268 Entry<?, ?> entry = iterator.next(); 269 appendable.append(joiner.toString(entry.getKey())); 270 appendable.append(keyValueSeparator); 271 appendable.append(joiner.toString(entry.getValue())); 272 while (iterator.hasNext()) { 273 appendable.append(joiner.separator); 274 Entry<?, ?> e = iterator.next(); 275 appendable.append(joiner.toString(e.getKey())); 276 appendable.append(keyValueSeparator); 277 appendable.append(joiner.toString(e.getValue())); 278 } 279 } 280 return appendable; 281 } 282 283 /** 284 * Appends the string representation of each entry of {@code map}, using the 285 * previously configured separator and key-value separator, to {@code 286 * builder}. Identical to {@link #appendTo(Appendable, Map)}, except that it 287 * does not throw {@link IOException}. 288 */ 289 public StringBuilder appendTo(StringBuilder builder, Map<?, ?> map) { 290 try { 291 appendTo((Appendable) builder, map); 292 } catch (IOException impossible) { 293 throw new AssertionError(impossible); 294 } 295 return builder; 296 } 297 298 /** 299 * Returns a string containing the string representation of each entry of 300 * {@code map}, using the previously configured separator and key-value 301 * separator. 302 */ 303 public String join(Map<?, ?> map) { 304 return appendTo(new StringBuilder(), map).toString(); 305 } 306 307 /** 308 * Returns a map joiner with the same behavior as this one, except 309 * automatically substituting {@code nullText} for any provided null keys or 310 * values. 311 */ 312 public MapJoiner useForNull(String nullText) { 313 return new MapJoiner(joiner.useForNull(nullText), keyValueSeparator); 314 } 315 } 316 317 CharSequence toString(Object part) { 318 return (part instanceof CharSequence) 319 ? (CharSequence) part 320 : part.toString(); 321 } 322 323 private static Iterable<Object> iterable( 324 final Object first, final Object second, final Object[] rest) { 325 checkNotNull(rest); 326 return new AbstractList<Object>() { 327 @Override public int size() { 328 return rest.length + 2; 329 } 330 @Override public Object get(int index) { 331 switch (index) { 332 case 0: 333 return first; 334 case 1: 335 return second; 336 default: 337 return rest[index - 2]; 338 } 339 } 340 }; 341 } 342 } 343