001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017 MicroBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.configuration.api;
018
019import java.io.Serializable;
020
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Objects;
025
026/**
027 * A value for a configuration property, particularly as {@linkplain
028 * AmbiguousConfigurationValuesException#getValues() exposed} by an
029 * {@link AmbiguousConfigurationValuesException}.
030 *
031 * @author <a href="http://about.me/lairdnelson"
032 * target="_parent">Laird Nelson</a>
033 *
034 * @see AmbiguousConfigurationValuesException
035 */
036public class ConfigurationValue implements Serializable {
037
038
039  /*
040   * Static fields.
041   */
042
043
044  /**
045   * The version of this class for {@linkplain Serializable
046   * serialization} purposes.
047   */
048  private static final long serialVersionUID = 1L;
049
050
051  /*
052   * Instance fields.
053   */
054
055
056  /**
057   * The system that produced this {@link
058   * ConfigurationValue}.
059   *
060   * <p>This field may be {@code null}.</p>
061   */
062  private Serializable source;
063
064  /**
065   * A {@link Map} representing the specific configuration coordinates
066   * this {@link ConfigurationValue} is selected for.
067   *
068   * <p>This field may be {@code null}.</p>
069   */
070  private final Map<String, String> coordinates;
071
072  /**
073   * The name of the configuration property for which this {@link
074   * ConfigurationValue} is a value.
075   *
076   * <p>This field is never {@code null}.</p>
077   */
078  private final String name;
079
080  /**
081   * The actual configuration value represented by this {@link
082   * ConfigurationValue}.
083   *
084   * <p>This field may be {@code null}.</p>
085   */
086  private final String value;
087
088  /**
089   * Whether or not this {@link ConfigurationValue} is to be regarded
090   * as the authoritative value for the configuration property in
091   * question.
092   */
093  private final boolean authoritative;
094
095
096  /*
097   * Constructors.
098   */
099
100
101  /**
102   * Creates a new {@link ConfigurationValue}.
103   *
104   * @param source the system creating this {@link
105   * ConfigurationValue}; must not be {@code null}
106   *
107   * @param coordinates the configuration coordinates to which this
108   * {@link ConfigurationValue} applies; must be a subset of the
109   * configuration coordinates that resulted in this {@link
110   * ConfigurationValue} being created; may be {@code null}
111   *
112   * @param name the name of the configuration property for which this
113   * is a value; must not be {@code null}
114   *
115   * @param value the value; may be {@code null}
116   *
117   * @param authoritative whether this {@link ConfigurationValue} is
118   * to be considered authoritative
119   *
120   * @exception NullPointerException if either {@code source}
121   * or {@code name} is {@code null}
122   */
123  public ConfigurationValue(final Serializable source, final Map<String, String> coordinates, final String name, final String value, final boolean authoritative) {
124    super();
125    Objects.requireNonNull(source);
126    Objects.requireNonNull(name);
127    this.source = source;
128    if (coordinates == null || coordinates.isEmpty()) {
129      this.coordinates = Collections.emptyMap();
130    } else {
131      this.coordinates = Collections.unmodifiableMap(new HashMap<>(coordinates));
132    }
133    this.name = name;
134    this.value = value;
135    this.authoritative = authoritative;
136  }
137
138
139  /*
140   * Instance methods.
141   */
142
143
144  /**
145   * Returns the system that created this {@link ConfigurationValue}.
146   *
147   * <p>This method never returns {@code null}.</p>
148   *
149   * @return the system that created this {@link ConfigurationValue};
150   * never {@code null}
151   */
152  public final Object getSource() {
153    return this.source;
154  }
155
156  /**
157   * Returns the configuration coordinates locating this {@link
158   * ConfigurationValue} in configuration space.
159   *
160   * <p>This method may return {@code null}.</p>
161   *
162   * @return the configuration coordinates locating this {@link
163   * ConfigurationValue} in configuration space, or {@code null}
164   */
165  public final Map<String, String> getCoordinates() {
166    return this.coordinates;
167  }
168
169  /**
170   * Returns the name of the configuration property for which this
171   * {@link ConfigurationValue} is a value.
172   *
173   * <p>This method never returns {@code null}.</p>
174   *
175   * @return the name of the configuration property for which this
176   * {@link ConfigurationValue} is a value; never {@code null}
177   */
178  public final String getName() {
179    return this.name;
180  }
181
182  /**
183   * Returns the actual value that this {@link ConfigurationValue}
184   * represents.
185   *
186   * <p>This method may return {@code null}.</p>
187   *
188   * @return the actual value that this {@link ConfigurationValue}
189   * represents, or {@code null}
190   */
191  public final String getValue() {
192    return this.value;
193  }
194
195  /**
196   * Returns {@code true} if this {@link ConfigurationValue} is to be
197   * regarded as authoritative.
198   *
199   * @return {@code true} if this {@link ConfigurationValue} is to be
200   * regarded as authoritative; {@code false} otherwise
201   */
202  public final boolean isAuthoritative() {
203    return this.authoritative;
204  }
205
206  /**
207   * Returns the <em>specificity</em> of this {@link
208   * ConfigurationValue}.
209   *
210   * <p>The specificity of a {@link ConfigurationValue} is equal to
211   * the {@linkplain Map#size() size} of its {@linkplain
212   * #getCoordinates() configuration coordinates
213   * <code>Map</code>}.</p>
214   *
215   * @return the specificity of this {@link ConfigurationValue};
216   * always zero or a positive integer
217   */
218  public final int specificity() {
219    int size = 0;
220    final Map<?, ?> coordinates = this.getCoordinates();
221    if (coordinates != null) {
222      size = coordinates.size();
223    }
224    return size;
225  }
226
227  /**
228   * Returns a hashcode for this {@link ConfigurationValue}.
229   *
230   * @return a hashcode for this {@link ConfigurationValue}
231   */
232  @Override
233  public int hashCode() {
234    int hashCode = 17;
235
236    // Note: getSource() and isAuthoritative() are deliberately
237    // omitted from hashCode calculation.
238    
239    final Object coordinates = this.getCoordinates();
240    int c = coordinates == null ? 0 : coordinates.hashCode();
241    hashCode = 37 * hashCode + c;
242
243    final Object name = this.getName();
244    c = name == null ? 0 : name.hashCode();
245    hashCode = 37 * hashCode + c;
246
247    final Object value = this.getValue();
248    c = value == null ? 0 : value.hashCode();
249    hashCode = 37 * hashCode + c;
250    
251    return hashCode;
252  }
253
254  /**
255   * Returns {@code true} if the supplied {@link Object} is equal to
256   * this {@link ConfigurationValue}.
257   *
258   * <p>An {@link Object} is equal to a {@link ConfigurationValue}
259   * if:</p>
260   *
261   * <ul>
262   *
263   * <li>It is an instance of {@link ConfigurationValue}</li>
264   *
265   * <li>Its {@linkplain #getName() name} and {@link #getCoordinates()
266   * coordinates} are equal to those of the {@link ConfigurationValue}
267   * to which it is being compared</li>
268   *
269   * <li>Its {@linkplain #getValue() value} is equal to that of the
270   * {@link ConfigurationValue} to which it is being compared</li>
271   *
272   * </ul>
273   *
274   * @param other the {@link Object} to test; may be {@code null}
275   *
276   * @return {@code true} if the supplied {@link Object} is equal to
277   * this {@link ConfigurationValue}; {@code false} otherwise
278   */
279  @Override
280  public boolean equals(final Object other) {
281    if (other == this) {
282      return true;
283    } else if (other instanceof ConfigurationValue) {
284      final ConfigurationValue her = (ConfigurationValue)other;
285
286      // Note: getSource() and isAuthoritative() are
287      // deliberately omitted from the algorithm.
288      
289      final Object coordinates = this.getCoordinates();
290      if (coordinates == null) {
291        if (her.getCoordinates() != null) {
292          return false;
293        }
294      } else if (!coordinates.equals(her.getCoordinates())) {
295        return false;
296      }
297      
298      final Object name = this.getName();
299      if (name == null) {
300        if (her.getName() != null) {
301          return false;
302        }
303      } else if (!name.equals(her.getName())) {
304        return false;
305      }
306
307      final Object value = this.getValue();
308      if (value == null) {
309        if (her.getValue() != null) {
310          return false;
311        }
312      } else if (!value.equals(her.getValue())) {
313        return false;
314      }
315
316      return true;
317    } else {
318      return false;
319    }
320  }
321  
322  /**
323   * Returns a non-{@code null} {@link String} representation of this
324   * {@link ConfigurationValue}.
325   *
326   * <p>This method never returns {@code null}.</p>
327   *
328   * <p>Overrides of this method must not return {@code null}.</p>
329   *
330   * @return a non-{@code null} {@link String} representation of this
331   * {@link ConfigurationValue}
332   */
333  @Override
334  public String toString() {
335    final StringBuilder sb = new StringBuilder();
336    sb.append("(");
337    sb.append(this.getSource());
338    sb.append(") ");
339    final Map<String, String> coordinates = this.getCoordinates();
340    if (coordinates != null && !coordinates.isEmpty()) {
341      sb.append(coordinates);
342      sb.append(" ");
343    }
344    sb.append(this.getName());
345    sb.append("=");
346    sb.append(this.getValue());
347    final boolean authoritative = this.isAuthoritative();
348    if (authoritative) {
349      sb.append(" (authoritative)");
350    }
351    return sb.toString();
352  }
353  
354}