001/* 002 * Copyright 2023 the original author or authors. 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.tools.string; 017 018import static de.cuioss.tools.collect.CollectionLiterals.mutableList; 019import static de.cuioss.tools.collect.MoreCollections.isEmpty; 020import static de.cuioss.tools.string.MoreStrings.isBlank; 021import static java.util.Objects.requireNonNull; 022 023import java.util.ArrayList; 024import java.util.Iterator; 025import java.util.stream.Collectors; 026 027import de.cuioss.tools.logging.CuiLogger; 028import lombok.AccessLevel; 029import lombok.NonNull; 030import lombok.RequiredArgsConstructor; 031 032/** 033 * Inspired by Googles Joiner. 034 * <p> 035 * It uses internally the {@link String#join(CharSequence, Iterable)} 036 * implementation of java and provides a guava like wrapper. It focuses on the 037 * simplified Joining and omits the Map based variants. 038 * </p> 039 * <h2>Usage</h2> 040 * 041 * <pre> 042 * assertEquals("key=value", Joiner.on('=').join("key", "value")); 043 * assertEquals("key=no value", Joiner.on('=').useForNull("no value").join("key", null)); 044 * assertEquals("key", Joiner.on('=').skipNulls().join("key", null)); 045 * assertEquals("key", Joiner.on('=').skipEmptyStrings().join("key", "")); 046 * assertEquals("key", Joiner.on('=').skipBlankStrings().join("key", " ")); 047 * </pre> 048 * 049 * <h2>Migrating from Guava</h2> 050 * <p> 051 * In order to migrate for most case you only need to replace the package name 052 * on the import. 053 * </p> 054 * <h2>Changes to Guavas-Joiner</h2> 055 * <p> 056 * In case of content to be joined containing {@code null}-values and not set to 057 * skip nulls, {@link #skipNulls()} it does not throw an 058 * {@link NullPointerException} but writes "null" for each {@code null} element. 059 * You can define a different String by calling {@link #useForNull(String)} 060 * </p> 061 * <p> 062 * In addition to {@link #skipEmptyStrings()} it provides a variant 063 * {@link #skipBlankStrings()} 064 * </p> 065 * 066 * @author Oliver Wolff 067 * 068 */ 069@RequiredArgsConstructor(access = AccessLevel.MODULE) 070public final class Joiner { 071 072 private static final CuiLogger log = new CuiLogger(Joiner.class); 073 074 @NonNull 075 private final JoinerConfig joinerConfig; 076 077 /** 078 * Returns a Joiner that uses the given fixed string as a separator. For 079 * example, {@code 080 * Joiner.on("-").join("foo", "bar")} returns a String "foo-bar" 081 * 082 * @param separator the literal, nonempty string to recognize as a separator 083 * 084 * @return a {@link Joiner}, with default settings, that uses that separator 085 */ 086 public static Joiner on(final String separator) { 087 requireNonNull(separator); 088 return new Joiner(JoinerConfig.builder().separator(separator).build()); 089 } 090 091 /** 092 * Returns a Joiner that uses the given fixed string as a separator. For 093 * example, {@code 094 * Joiner.on('-').join("foo", "bar")} returns a String "foo-bar" 095 * 096 * @param separator the literal, nonempty string to recognize as a separator 097 * 098 * @return a {@link Joiner}, with default settings, that uses that separator 099 */ 100 public static Joiner on(final char separator) { 101 requireNonNull(separator); 102 return on(String.valueOf(separator)); 103 } 104 105 /** 106 * @param nullText to be used as substitution for {@code null} elements 107 * @return a joiner with the same behavior as this one, except automatically 108 * substituting {@code 109 * nullText} for any provided null elements. 110 */ 111 public Joiner useForNull(final String nullText) { 112 return new Joiner(joinerConfig.copy().useForNull(nullText).build()); 113 } 114 115 /** 116 * @return a joiner with the same behavior as this one, except automatically 117 * skipping null-values 118 */ 119 public Joiner skipNulls() { 120 return new Joiner(joinerConfig.copy().skipNulls(true).build()); 121 } 122 123 /** 124 * @return a joiner with the same behavior as this one, except automatically 125 * skipping String-values that evaluate to an empty String 126 */ 127 public Joiner skipEmptyStrings() { 128 return new Joiner(joinerConfig.copy().skipEmpty(true).build()); 129 } 130 131 /** 132 * @return a joiner with the same behavior as this one, except automatically 133 * skipping String-values that evaluate to a blank String as defined 134 * within {@link MoreStrings#isBlank(CharSequence)} 135 */ 136 public Joiner skipBlankStrings() { 137 return new Joiner(joinerConfig.copy().skipBlank(true).build()); 138 } 139 140 /** 141 * @param parts to be joined 142 * 143 * @return a string containing the string representation of each of 144 * {@code parts}, using the previously configured separator between 145 * each. 146 */ 147 public String join(Iterable<?> parts) { 148 return doJoin(parts); 149 } 150 151 /** 152 * @param parts to be joined 153 * 154 * @return a string containing the string representation of each of 155 * {@code parts}, using the previously configured separator between 156 * each. 157 */ 158 public String join(Iterator<?> parts) { 159 return doJoin(mutableList(parts)); 160 } 161 162 /** 163 * @param parts to be joined 164 * @return a string containing the string representation of each of 165 * {@code parts}, using the previously configured separator between 166 * each. 167 */ 168 public String join(Object... parts) { 169 return doJoin(mutableList(parts)); 170 } 171 172 private String doJoin(Iterable<?> parts) { 173 log.trace("Joining elements with configuration {}", joinerConfig); 174 if (isEmpty(parts)) { 175 return ""; 176 } 177 var builder = new ArrayList<CharSequence>(); 178 for (Object element : parts) { 179 if (null == element) { 180 if (!joinerConfig.isSkipNulls()) { 181 builder.add(joinerConfig.getUseForNull()); 182 } 183 } else if (element instanceof CharSequence sequence) { 184 builder.add(sequence); 185 } else { 186 builder.add(MoreStrings.lenientToString(element)); 187 } 188 } 189 if (joinerConfig.isSkipEmpty()) { 190 builder = builder.stream().filter(element -> !MoreStrings.isEmpty(element)) 191 .collect(Collectors.toCollection(ArrayList::new)); 192 } 193 194 if (joinerConfig.isSkipBlank()) { 195 builder = builder.stream().filter(element -> !isBlank(element)) 196 .collect(Collectors.toCollection(ArrayList::new)); 197 } 198 return String.join(joinerConfig.getSeparator(), builder); 199 } 200 201}