001package react4j;
002
003import java.util.HashMap;
004import java.util.Map;
005import java.util.Objects;
006import javax.annotation.Nonnull;
007import javax.annotation.Nullable;
008import static org.realityforge.braincheck.Guards.*;
009
010/**
011 * A global registry containing the react contexts that have been created.
012 * A react context is expected to be registered with a java type and an optional qualifier.
013 * This allows multiple contexts of the same type to be present in the global registry.
014 */
015public final class Contexts
016{
017  /**
018   * The default qualifier if not otherwise specified.
019   */
020  @Nonnull
021  private static final String DEFAULT_QUALIFIER = "";
022  /**
023   * The map containing all the contexts. See the class javadocs for further details.
024   */
025  @Nonnull
026  private static final Map<Class<?>, Map<String, Context<?>>> c_contexts = new HashMap<>();
027  /**
028   * Context provider.
029   */
030  @Nonnull
031  private static ContextProvider c_contextProvider = new DefaultContextProvider();
032
033  private Contexts()
034  {
035  }
036
037  /**
038   * Register the context with the specified type, an empty "" qualifier and a null default value.
039   * A context with matching type and qualifier parameters must not have been already registered.
040   *
041   * @param type the type of the context value.
042   * @param <T>  the type of the context value.
043   * @see #register(Class, String, Object)
044   */
045  public static <T> void register( @Nonnull final Class<T> type )
046  {
047    register( type, DEFAULT_QUALIFIER, null );
048  }
049
050  /**
051   * Register the context with the specified type, an empty "" qualifier and a default value.
052   * A context with matching type and qualifier parameters must not have been already registered.
053   *
054   * @param type         the type of the context value.
055   * @param defaultValue the default value to return if no provider has been specified higher in the tree.
056   * @param <T>          the type of the context value.
057   * @see #register(Class, String, Object)
058   */
059  public static <T> void register( @Nonnull final Class<T> type, @Nullable final T defaultValue )
060  {
061    register( type, DEFAULT_QUALIFIER, defaultValue );
062  }
063
064  /**
065   * Register the context with the specified type, qualifier and a null default value.
066   * A context with matching type and qualifier parameters must not have been already registered.
067   *
068   * @param type      the type of the context value.
069   * @param qualifier the qualifier to distinguish multiple instances of the the same type.
070   * @param <T>       the type of the context value.
071   * @see #register(Class, String, Object)
072   */
073  public static <T> void register( @Nonnull final Class<T> type, @Nonnull final String qualifier )
074  {
075    register( type, qualifier, null );
076  }
077
078  /**
079   * Register the context with the specified type, qualifier and a default value.
080   * A context with matching type and qualifier parameters must not have been already registered.
081   *
082   * @param type         the type of the context value.
083   * @param qualifier    the qualifier to distinguish multiple instances of the the same type.
084   * @param defaultValue the default value to return if no provider has been specified higher in the tree.
085   * @param <T>          the type of the context value.
086   */
087  public static <T> void register( @Nonnull final Class<T> type,
088                                   @Nonnull final String qualifier,
089                                   @Nullable final T defaultValue )
090  {
091    if ( ReactConfig.shouldCheckInvariants() )
092    {
093      apiInvariant( () -> !type.isPrimitive(),
094                    () -> "Attempting to register primitive type " + type +
095                          " in React context. Use the boxed type instead" );
096    }
097    final Map<String, Context<?>> map = c_contexts.computeIfAbsent( type, t -> new HashMap<>() );
098    if ( ReactConfig.shouldCheckInvariants() )
099    {
100      apiInvariant( () -> !map.containsKey( qualifier ),
101                    () -> "Attempting to register React context with type " + type +
102                          " and qualifier '" + qualifier + "' but a context already exists with that type and name" );
103    }
104    assert !map.containsKey( qualifier );
105    map.put( qualifier, c_contextProvider.createContext( defaultValue ) );
106  }
107
108  /**
109   * Return the context with the specified type and an empty "" qualifier.
110   * A context with matching type and qualifier parameters must have already been registered.
111   *
112   * @param type the type of the context value.
113   * @param <T>  the type of the context value.
114   * @return the context.
115   * @see #get(Class, String)
116   */
117  public static <T> Context<T> get( @Nonnull final Class<T> type )
118  {
119    return get( type, DEFAULT_QUALIFIER );
120  }
121
122  /**
123   * Return the context with the specified type and qualifier.
124   * A context with matching type and qualifier parameters must have already been registered.
125   *
126   * @param type      the type of the context value.
127   * @param qualifier the qualifier to distinguish multiple instances of the the same type.
128   * @param <T>       the type of the context value.
129   * @return the context.
130   * @see #get(Class)
131   */
132  @SuppressWarnings( "unchecked" )
133  public static <T> Context<T> get( @Nonnull final Class<T> type, @Nonnull final String qualifier )
134  {
135    if ( ReactConfig.shouldCheckInvariants() )
136    {
137      apiInvariant( () -> !type.isPrimitive(),
138                    () -> "Attempting to access primitive type " + type +
139                          " from the React context. Access using the boxed type instead" );
140    }
141    final Map<String, Context<?>> map = c_contexts.get( type );
142    if ( ReactConfig.shouldCheckInvariants() )
143    {
144      apiInvariant( () -> null != map && map.containsKey( qualifier ),
145                    () -> "Attempting to retrieve React context with type " + type +
146                          " and qualifier '" + qualifier + "' but no such context exists with that type and name" );
147    }
148    assert null != map;
149    assert map.containsKey( qualifier );
150    return (Context<T>) map.get( qualifier );
151  }
152
153  static void setContextProvider( @Nonnull final ContextProvider contextProvider )
154  {
155    c_contextProvider = Objects.requireNonNull( contextProvider );
156  }
157
158  static void clear()
159  {
160    c_contexts.clear();
161  }
162
163  /**
164   * Interface used to provide context. This can be switched out as part of testing.
165   */
166  interface ContextProvider
167  {
168    <T> Context<T> createContext( @Nullable T defaultValue );
169  }
170
171  static class DefaultContextProvider
172    implements ContextProvider
173  {
174    @Override
175    public <T> Context<T> createContext( @Nullable final T defaultValue )
176    {
177      return React.createContext( defaultValue );
178    }
179  }
180}