001/* 002 * Copyright 2012-2017 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 String toString() { 064 return (this.message == null ? "" : this.message); 065 } 066 067 @Override 068 public int hashCode() { 069 return ObjectUtils.nullSafeHashCode(this.message); 070 } 071 072 @Override 073 public boolean equals(Object obj) { 074 if (obj == null || !ConditionMessage.class.isInstance(obj)) { 075 return false; 076 } 077 if (obj == this) { 078 return true; 079 } 080 return ObjectUtils.nullSafeEquals(((ConditionMessage) obj).message, 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, 362 items == null ? (Collection<?>) null : Arrays.asList(items)); 363 } 364 365 /** 366 * Indicate the items. For example 367 * {@code didNotFind("bean", "beans").items(Collections.singleton("x")} results in 368 * the message "did not find bean x". 369 * @param items the source of the items (may be {@code null}) 370 * @return a built {@link ConditionMessage} 371 */ 372 public ConditionMessage items(Collection<?> items) { 373 return items(Style.NORMAL, items); 374 } 375 376 /** 377 * Indicate the items with a {@link Style}. For example 378 * {@code didNotFind("bean", "beans").items(Style.QUOTE, Collections.singleton("x")} 379 * results in the message "did not find bean 'x'". 380 * @param style the render style 381 * @param items the source of the items (may be {@code null}) 382 * @return a built {@link ConditionMessage} 383 */ 384 public ConditionMessage items(Style style, Collection<?> items) { 385 Assert.notNull(style, "Style must not be null"); 386 StringBuilder message = new StringBuilder(this.reason); 387 items = style.applyTo(items); 388 if ((this.condition == null || items.size() <= 1) 389 && StringUtils.hasLength(this.singular)) { 390 message.append(" " + this.singular); 391 } 392 else if (StringUtils.hasLength(this.plural)) { 393 message.append(" " + this.plural); 394 } 395 if (items != null && !items.isEmpty()) { 396 message.append( 397 " " + StringUtils.collectionToDelimitedString(items, ", ")); 398 } 399 return this.condition.because(message.toString()); 400 } 401 402 } 403 404 /** 405 * Render styles. 406 */ 407 public enum Style { 408 409 NORMAL { 410 @Override 411 protected Object applyToItem(Object item) { 412 return item; 413 } 414 }, 415 416 QUOTE { 417 @Override 418 protected String applyToItem(Object item) { 419 return (item == null ? null : "'" + item + "'"); 420 } 421 }; 422 423 public Collection<?> applyTo(Collection<?> items) { 424 List<Object> result = new ArrayList<Object>(); 425 for (Object item : items) { 426 result.add(applyToItem(item)); 427 } 428 return result; 429 } 430 431 protected abstract Object applyToItem(Object item); 432 433 } 434 435}