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 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}