001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2022 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.loader.jackson;
018
019import java.util.Collection;
020import java.util.Iterator;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Objects;
026import java.util.TreeSet;
027
028import com.fasterxml.jackson.annotation.JsonAnySetter;
029import com.fasterxml.jackson.annotation.JsonAutoDetect;
030
031import org.microbean.development.annotation.Experimental;
032import org.microbean.development.annotation.Incomplete;
033
034import org.microbean.qualifier.Qualifier;
035import org.microbean.qualifier.Qualifiers;
036
037/**
038 * A Java object that represents a tree-like configuration structure,
039 * using Jackson mapping constructs to make things easier.
040 *
041 * @author <a href="https://about.me/lairdnelson"
042 * target="_parent">Laird Nelson</a>
043 */
044@Experimental
045@Incomplete
046@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE,
047                fieldVisibility = JsonAutoDetect.Visibility.NONE,
048                getterVisibility = JsonAutoDetect.Visibility.NONE,
049                isGetterVisibility = JsonAutoDetect.Visibility.NONE,
050                setterVisibility = JsonAutoDetect.Visibility.NONE)
051public class Configuration {
052
053  private Map<? extends String, ?> qualifiersMap;
054
055  private final Map<String, Object> any;
056
057  /**
058   * Creates a new {@link Configuration}.
059   */
060  public Configuration() {
061    super();
062    this.qualifiersMap = Map.of();
063    this.any = new LinkedHashMap<>();
064  }
065
066  /**
067   * Returns a {@link Qualifiers} representing all qualifiers in the
068   * {@link Configuration}.
069   *
070   * @return a {@link Qualifiers} representing all qualifiers in the
071   * {@link Configuration}
072   *
073   * @nullability This method never returns {@code null}.
074   *
075   * @idempotency This method is idempotent and deterministic.
076   *
077   * @threadsafety This method is <strong>not</strong> safe for
078   * concurrent use by multiple threads.
079   */
080  public final Qualifiers<? extends String, ?> qualifiers() {
081    return this.qualifiers(List.of());
082  }
083
084  /**
085   * Returns a {@link Qualifiers} representing all qualifiers in the
086   * {@link Configuration} associated with the supplied sequence of
087   * names.
088   *
089   * @param names a sequence of names identifying a path to the root
090   * of a tree of qualifiers; may be {@code null}
091   *
092   * @return a {@link Qualifiers} representing all qualifiers in the
093   * {@link Configuration} associated with the supplied sequence of
094   * names
095   *
096   * @nullability This method never returns {@code null}.
097   *
098   * @idempotency This method is idempotent and deterministic.
099   *
100   * @threadsafety This method is <strong>not</strong> safe for
101   * concurrent use by multiple threads.
102   */
103  @SuppressWarnings("unchecked")
104  public final Qualifiers<? extends String, ?> qualifiers(final Iterable<? extends String> names) {
105    Map<? extends String, ?> map = this.qualifiersMap;
106    if (map == null || map.isEmpty()) {
107      return Qualifiers.of();
108    }
109    map = new LinkedHashMap<>(map);
110    if (names != null) {
111      for (final String name : names) {
112        Object value = map.get(name);
113        if (value == null) {
114          return Qualifiers.of();
115        } else if (value instanceof Map) {
116          map = (Map<? extends String, ?>)value;
117        }
118      }
119    }
120    final Collection<Qualifier<String, Object>> c = new TreeSet<>();
121    for (final Entry<? extends String, ?> e : map.entrySet()) {
122      final Object value = e.getValue();
123      if (!(value instanceof Map)) {
124        c.add(Qualifier.of(e.getKey(), value));
125      }
126    }
127    return Qualifiers.of(c);
128  }
129
130  /**
131   * Returns an {@link Object} in this {@link Configuration} indexed
132   * under the supplied {@code name}.
133   *
134   * @param name the name; must not be {@code null}
135   *
136   * @return the associated {@link Object}, or {@code null}
137   *
138   * @nullability This method may return {@code null}.
139   *
140   * @idempotency This method is idempotent and deterministic.
141   *
142   * @threadsafety This method is <strong>not</strong> safe for
143   * concurrent use by multiple threads.
144   */
145  protected final Object any(final String name) {
146    return this.any.get(name);
147  }
148
149  @JsonAnySetter
150  @SuppressWarnings("unchecked")
151  private final void any(final String name, final Object value) {
152    if (name.equals("@qualifiers")) {
153      if (value == null) {
154        this.qualifiersMap = Map.of();
155      } else if (value instanceof Map) {
156        this.qualifiersMap = (Map<? extends String, ?>)value;
157      } else {
158        this.any.put(name, value);
159      }
160    } else {
161      this.any.put(name, value);
162    }
163  }
164
165  /**
166   * Returns a hashcode for this {@link Configuration}.
167   *
168   * @return a hashcode for this {@link Configuration}
169   *
170   * @idempotency This method is, and its overrides must be,
171   * idempotent and deterministic.
172   *
173   * @threadsafety This method is <strong>not</strong> safe for
174   * concurrent use by multiple threads.
175   */
176  @Override // Object
177  public int hashCode() {
178    return Objects.hash(this.qualifiersMap, this.any);
179  }
180
181  /**
182   * Returns {@code true} if the supplied {@link Object} is equal to
183   * this {@link Configuration}.
184   *
185   * @param other the {@link Object} to test; may be {@code null}
186   *
187   * @return {@code true} if the supplied {@link Object} is equal to
188   * this {@link Configuration}
189   *
190   * @idempotency This method is, and its overrides must be,
191   * idempotent and deterministic.
192   *
193   * @threadsafety This method is <strong>not</strong> safe for
194   * concurrent use by multiple threads.
195   */
196  @Override // Object
197  public boolean equals(final Object other) {
198    if (other == this) {
199      return true;
200    } else if (other != null && other.getClass() == this.getClass()) {
201      final Configuration her = (Configuration)other;
202      return
203        Objects.equals(this.qualifiersMap, her.qualifiersMap) &&
204        Objects.equals(this.any, her.any);
205    } else {
206      return false;
207    }
208  }
209
210  /**
211   * Returns a {@link String} representation of this {@link
212   * Configuration}.
213   *
214   * <p>The format of the returned {@link String} is deliberately
215   * undefined and subject to change between revisions of this class
216   * without notice.</p>
217   *
218   * @return a {@link String} representation of this {@link
219   * Configuration}
220   *
221   * @nullability This method does not, and its overrides must not,
222   * return {@code null}.
223   *
224   * @idempotency This method is, and its overrides must be,
225   * idempotent and deterministic.
226   *
227   * @threadsafety This method is <strong>not</strong> safe for
228   * concurrent use by multiple threads.
229   */
230  @Override // Object
231  public String toString() {
232    return this.qualifiersMap + " " + this.any;
233  }
234
235}