001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
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 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
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 */
016
017package org.springframework.boot.autoconfigure.condition;
018
019import java.lang.annotation.Annotation;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.List;
025
026import org.springframework.util.Assert;
027import org.springframework.util.ClassUtils;
028import org.springframework.util.ObjectUtils;
029import org.springframework.util.StringUtils;
030
031/**
032 * A message associated with a {@link ConditionOutcome}. Provides a fluent builder style
033 * API to encourage consistency across all condition messages.
034 *
035 * @author Phillip Webb
036 * @since 1.4.1
037 */
038public final class ConditionMessage {
039
040        private String message;
041
042        private ConditionMessage() {
043                this(null);
044        }
045
046        private ConditionMessage(String message) {
047                this.message = message;
048        }
049
050        private ConditionMessage(ConditionMessage prior, String message) {
051                this.message = prior.isEmpty() ? message : prior + "; " + message;
052        }
053
054        /**
055         * Return {@code true} if the message is empty.
056         * @return if the message is empty
057         */
058        public boolean isEmpty() {
059                return !StringUtils.hasLength(this.message);
060        }
061
062        @Override
063        public boolean equals(Object obj) {
064                if (obj == null || !ConditionMessage.class.isInstance(obj)) {
065                        return false;
066                }
067                if (obj == this) {
068                        return true;
069                }
070                return ObjectUtils.nullSafeEquals(((ConditionMessage) obj).message, this.message);
071        }
072
073        @Override
074        public int hashCode() {
075                return ObjectUtils.nullSafeHashCode(this.message);
076        }
077
078        @Override
079        public String toString() {
080                return (this.message != null) ? this.message : "";
081        }
082
083        /**
084         * Return a new {@link ConditionMessage} based on the instance and an appended
085         * message.
086         * @param message the message to append
087         * @return a new {@link ConditionMessage} instance
088         */
089        public ConditionMessage append(String message) {
090                if (!StringUtils.hasLength(message)) {
091                        return this;
092                }
093                if (!StringUtils.hasLength(this.message)) {
094                        return new ConditionMessage(message);
095                }
096
097                return new ConditionMessage(this.message + " " + message);
098        }
099
100        /**
101         * Return a new builder to construct a new {@link ConditionMessage} based on the
102         * instance and a new condition outcome.
103         * @param condition the condition
104         * @param details details of the condition
105         * @return a {@link Builder} builder
106         * @see #andCondition(String, Object...)
107         * @see #forCondition(Class, Object...)
108         */
109        public Builder andCondition(Class<? extends Annotation> condition,
110                        Object... details) {
111                Assert.notNull(condition, "Condition must not be null");
112                return andCondition("@" + ClassUtils.getShortName(condition), details);
113        }
114
115        /**
116         * Return a new builder to construct a new {@link ConditionMessage} based on the
117         * instance and a new condition outcome.
118         * @param condition the condition
119         * @param details details of the condition
120         * @return a {@link Builder} builder
121         * @see #andCondition(Class, Object...)
122         * @see #forCondition(String, Object...)
123         */
124        public Builder andCondition(String condition, Object... details) {
125                Assert.notNull(condition, "Condition must not be null");
126                String detail = StringUtils.arrayToDelimitedString(details, " ");
127                if (StringUtils.hasLength(detail)) {
128                        return new Builder(condition + " " + detail);
129                }
130                return new Builder(condition);
131        }
132
133        /**
134         * Factory method to return a new empty {@link ConditionMessage}.
135         * @return a new empty {@link ConditionMessage}
136         */
137        public static ConditionMessage empty() {
138                return new ConditionMessage();
139        }
140
141        /**
142         * Factory method to create a new {@link ConditionMessage} with a specific message.
143         * @param message the source message (may be a format string if {@code args} are
144         * specified)
145         * @param args format arguments for the message
146         * @return a new {@link ConditionMessage} instance
147         */
148        public static ConditionMessage of(String message, Object... args) {
149                if (ObjectUtils.isEmpty(args)) {
150                        return new ConditionMessage(message);
151                }
152                return new ConditionMessage(String.format(message, args));
153        }
154
155        /**
156         * Factory method to create a new {@link ConditionMessage} comprised of the specified
157         * messages.
158         * @param messages the source messages (may be {@code null})
159         * @return a new {@link ConditionMessage} instance
160         */
161        public static ConditionMessage of(Collection<? extends ConditionMessage> messages) {
162                ConditionMessage result = new ConditionMessage();
163                if (messages != null) {
164                        for (ConditionMessage message : messages) {
165                                result = new ConditionMessage(result, message.toString());
166                        }
167                }
168                return result;
169        }
170
171        /**
172         * Factory method for a builder to construct a new {@link ConditionMessage} for a
173         * condition.
174         * @param condition the condition
175         * @param details details of the condition
176         * @return a {@link Builder} builder
177         * @see #forCondition(String, Object...)
178         * @see #andCondition(String, Object...)
179         */
180        public static Builder forCondition(Class<? extends Annotation> condition,
181                        Object... details) {
182                return new ConditionMessage().andCondition(condition, details);
183        }
184
185        /**
186         * Factory method for a builder to construct a new {@link ConditionMessage} for a
187         * condition.
188         * @param condition the condition
189         * @param details details of the condition
190         * @return a {@link Builder} builder
191         * @see #forCondition(Class, Object...)
192         * @see #andCondition(String, Object...)
193         */
194        public static Builder forCondition(String condition, Object... details) {
195                return new ConditionMessage().andCondition(condition, details);
196        }
197
198        /**
199         * Builder used to create a {@link ConditionMessage} for a condition.
200         */
201        public final class Builder {
202
203                private final String condition;
204
205                private Builder(String condition) {
206                        this.condition = condition;
207                }
208
209                /**
210                 * Indicate that an exact result was found. For example
211                 * {@code foundExactly("foo")} results in the message "found foo".
212                 * @param result the result that was found
213                 * @return a built {@link ConditionMessage}
214                 */
215                public ConditionMessage foundExactly(Object result) {
216                        return found("").items(result);
217                }
218
219                /**
220                 * Indicate that one or more results were found. For example
221                 * {@code found("bean").items("x")} results in the message "found bean x".
222                 * @param article the article found
223                 * @return an {@link ItemsBuilder}
224                 */
225                public ItemsBuilder found(String article) {
226                        return found(article, article);
227                }
228
229                /**
230                 * Indicate that one or more results were found. For example
231                 * {@code found("bean", "beans").items("x", "y")} results in the message "found
232                 * beans x, y".
233                 * @param singular the article found in singular form
234                 * @param plural the article found in plural form
235                 * @return an {@link ItemsBuilder}
236                 */
237                public ItemsBuilder found(String singular, String plural) {
238                        return new ItemsBuilder(this, "found", singular, plural);
239                }
240
241                /**
242                 * Indicate that one or more results were not found. For example
243                 * {@code didNotFind("bean").items("x")} results in the message "did not find bean
244                 * x".
245                 * @param article the article found
246                 * @return an {@link ItemsBuilder}
247                 */
248                public ItemsBuilder didNotFind(String article) {
249                        return didNotFind(article, article);
250                }
251
252                /**
253                 * Indicate that one or more results were found. For example
254                 * {@code didNotFind("bean", "beans").items("x", "y")} results in the message "did
255                 * not find beans x, y".
256                 * @param singular the article found in singular form
257                 * @param plural the article found in plural form
258                 * @return an {@link ItemsBuilder}
259                 */
260                public ItemsBuilder didNotFind(String singular, String plural) {
261                        return new ItemsBuilder(this, "did not find", singular, plural);
262                }
263
264                /**
265                 * Indicates a single result. For example {@code resultedIn("yes")} results in the
266                 * message "resulted in yes".
267                 * @param result the result
268                 * @return a built {@link ConditionMessage}
269                 */
270                public ConditionMessage resultedIn(Object result) {
271                        return because("resulted in " + result);
272                }
273
274                /**
275                 * Indicates something is available. For example {@code available("money")}
276                 * results in the message "money is available".
277                 * @param item the item that is available
278                 * @return a built {@link ConditionMessage}
279                 */
280                public ConditionMessage available(String item) {
281                        return because(item + " is available");
282                }
283
284                /**
285                 * Indicates something is not available. For example {@code notAvailable("time")}
286                 * results in the message "time is not available".
287                 * @param item the item that is not available
288                 * @return a built {@link ConditionMessage}
289                 */
290                public ConditionMessage notAvailable(String item) {
291                        return because(item + " is not available");
292                }
293
294                /**
295                 * Indicates the reason. For example {@code reason("running Linux")} results in
296                 * the message "running Linux".
297                 * @param reason the reason for the message
298                 * @return a built {@link ConditionMessage}
299                 */
300                public ConditionMessage because(String reason) {
301                        if (StringUtils.isEmpty(reason)) {
302                                return new ConditionMessage(ConditionMessage.this, this.condition);
303                        }
304                        return new ConditionMessage(ConditionMessage.this, this.condition
305                                        + (StringUtils.isEmpty(this.condition) ? "" : " ") + reason);
306                }
307
308        }
309
310        /**
311         * Builder used to create a {@link ItemsBuilder} for a condition.
312         */
313        public final class ItemsBuilder {
314
315                private final Builder condition;
316
317                private final String reason;
318
319                private final String singular;
320
321                private final String plural;
322
323                private ItemsBuilder(Builder condition, String reason, String singular,
324                                String plural) {
325                        this.condition = condition;
326                        this.reason = reason;
327                        this.singular = singular;
328                        this.plural = plural;
329                }
330
331                /**
332                 * Used when no items are available. For example
333                 * {@code didNotFind("any beans").atAll()} results in the message "did not find
334                 * any beans".
335                 * @return a built {@link ConditionMessage}
336                 */
337                public ConditionMessage atAll() {
338                        return items(Collections.emptyList());
339                }
340
341                /**
342                 * Indicate the items. For example
343                 * {@code didNotFind("bean", "beans").items("x", "y")} results in the message "did
344                 * not find beans x, y".
345                 * @param items the items (may be {@code null})
346                 * @return a built {@link ConditionMessage}
347                 */
348                public ConditionMessage items(Object... items) {
349                        return items(Style.NORMAL, items);
350                }
351
352                /**
353                 * Indicate the items. For example
354                 * {@code didNotFind("bean", "beans").items("x", "y")} results in the message "did
355                 * not find beans x, y".
356                 * @param style the render style
357                 * @param items the items (may be {@code null})
358                 * @return a built {@link ConditionMessage}
359                 */
360                public ConditionMessage items(Style style, Object... items) {
361                        return items(style, (items != null) ? Arrays.asList(items) : null);
362                }
363
364                /**
365                 * Indicate the items. For example
366                 * {@code didNotFind("bean", "beans").items(Collections.singleton("x")} results in
367                 * the message "did not find bean x".
368                 * @param items the source of the items (may be {@code null})
369                 * @return a built {@link ConditionMessage}
370                 */
371                public ConditionMessage items(Collection<?> items) {
372                        return items(Style.NORMAL, items);
373                }
374
375                /**
376                 * Indicate the items with a {@link Style}. For example
377                 * {@code didNotFind("bean", "beans").items(Style.QUOTE, Collections.singleton("x")}
378                 * results in the message "did not find bean 'x'".
379                 * @param style the render style
380                 * @param items the source of the items (may be {@code null})
381                 * @return a built {@link ConditionMessage}
382                 */
383                public ConditionMessage items(Style style, Collection<?> items) {
384                        Assert.notNull(style, "Style must not be null");
385                        StringBuilder message = new StringBuilder(this.reason);
386                        items = style.applyTo(items);
387                        if ((this.condition == null || items.size() <= 1)
388                                        && StringUtils.hasLength(this.singular)) {
389                                message.append(" " + this.singular);
390                        }
391                        else if (StringUtils.hasLength(this.plural)) {
392                                message.append(" " + this.plural);
393                        }
394                        if (items != null && !items.isEmpty()) {
395                                message.append(
396                                                " " + StringUtils.collectionToDelimitedString(items, ", "));
397                        }
398                        return this.condition.because(message.toString());
399                }
400
401        }
402
403        /**
404         * Render styles.
405         */
406        public enum Style {
407
408                NORMAL {
409                        @Override
410                        protected Object applyToItem(Object item) {
411                                return item;
412                        }
413                },
414
415                QUOTE {
416                        @Override
417                        protected String applyToItem(Object item) {
418                                return (item != null) ? "'" + item + "'" : null;
419                        }
420                };
421
422                public Collection<?> applyTo(Collection<?> items) {
423                        List<Object> result = new ArrayList<>();
424                        for (Object item : items) {
425                                result.add(applyToItem(item));
426                        }
427                        return result;
428                }
429
430                protected abstract Object applyToItem(Object item);
431
432        }
433
434}