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}