001package arez.persist.runtime;
002
003import arez.Arez;
004import arez.Disposable;
005import arez.annotations.ArezComponent;
006import arez.annotations.ComponentDependency;
007import arez.annotations.ComponentId;
008import arez.annotations.PreDispose;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.HashMap;
012import java.util.Map;
013import java.util.Objects;
014import javax.annotation.Nonnull;
015import javax.annotation.Nullable;
016import static org.realityforge.braincheck.Guards.*;
017
018/**
019 * A scope is used to control the lifecycle of state storage.
020 * State containing within a scope can be removed from the store using a single call
021 * to {@link ArezPersist#releaseScope(Scope)}. Scopes can be nested within other scopes
022 * and releasing a scope, releases all child scopes. There is a single root scope in which
023 * all other scopes are nested and it can be retrieved using the {@link ArezPersist#getRootScope()}
024 * method.
025 *
026 * <p>A scope may be disposed. It is no longer valid to create nested scopes, store state or retrieve
027 * state with disposed scopes.</p>
028 */
029@ArezComponent
030public abstract class Scope
031{
032  /**
033   * The name of the root scope.
034   */
035  @Nonnull
036  public static final String ROOT_SCOPE_NAME = "<>";
037  /**
038   * The parent scope. Every scope but the root scope must contain a parent.
039   */
040  @ComponentDependency
041  @Nullable
042  final Scope _parent;
043  /**
044   * The name of the scope. Scopes have alphanumeric names and may also include '-' or '_' characters.
045   */
046  @Nonnull
047  private final String _name;
048  /**
049   * Nested scopes if any.
050   */
051  @Nonnull
052  private final Map<String, Scope> _nestedScopes = new HashMap<>();
053
054  @Nonnull
055  static Scope create( @Nullable final Scope parent, @Nonnull final String name )
056  {
057    return new Arez_Scope( parent, name );
058  }
059
060  Scope( @Nullable final Scope parent, @Nonnull final String name )
061  {
062    assert ( null == parent && ROOT_SCOPE_NAME.equals( name ) ) ||
063           ( null != parent && !ROOT_SCOPE_NAME.equals( name ) );
064    _parent = parent;
065    _name = Objects.requireNonNull( name );
066  }
067
068  /**
069   * Return the simple name of the scope.
070   * The simple name is the name that was used to create the scope.
071   *
072   * @return the simple name of the scope.
073   */
074  @Nonnull
075  public String getName()
076  {
077    return _name;
078  }
079
080  /**
081   * Return the qualified name of the scope.
082   * The qualified name includes the parent scopes name followed by a "." character unless
083   * the parent scope is the root scope.
084   *
085   * @return the qualified name of the scope.
086   */
087  @ComponentId
088  @Nonnull
089  public String getQualifiedName()
090  {
091    return null == _parent || null == _parent._parent ? _name : _parent.getQualifiedName() + "." + _name;
092  }
093
094  /**
095   * Find or create a scope directly nested under the current scope.
096   * This must not be invoked on a disposed scope.
097   *
098   * @param name the name of the nested scope.
099   * @return the scope.
100   */
101  @Nonnull
102  public Scope findOrCreateScope( @Nonnull final String name )
103  {
104    if ( ArezPersist.shouldCheckApiInvariants() )
105    {
106      apiInvariant( () -> Disposable.isNotDisposed( this ),
107                    () -> "findOrCreateScope() invoked on disposed scope named '" + _name + "'" );
108      apiInvariant( () -> isValidName( name ),
109                    () -> "findOrCreateScope() invoked with name '" + name +
110                          "' but the name has invalid characters. Names must contain alphanumeric " +
111                          "characters, '-' or '_'" );
112    }
113    final Scope existing = findScope( name );
114    if ( null != existing )
115    {
116      return existing;
117    }
118    else
119    {
120      final Scope scope = Scope.create( this, name );
121      _nestedScopes.put( name, scope );
122      return scope;
123    }
124  }
125
126  @Override
127  public String toString()
128  {
129    if ( Arez.areNamesEnabled() )
130    {
131      return getQualifiedName();
132    }
133    else
134    {
135      return super.toString();
136    }
137  }
138
139  @Nullable
140  Scope getParent()
141  {
142    return _parent;
143  }
144
145  @Nullable
146  Scope findScope( @Nonnull final String name )
147  {
148    return _nestedScopes.get( name );
149  }
150
151  @Nonnull
152  public Collection<Scope> getNestedScopes()
153  {
154    final Collection<Scope> scopes = _nestedScopes.values();
155    return ArezPersist.shouldCheckApiInvariants() ? Collections.unmodifiableCollection( scopes ) : scopes;
156  }
157
158  @PreDispose
159  void preDispose()
160  {
161    assert _nestedScopes.isEmpty();
162    if ( null != _parent )
163    {
164      _parent._nestedScopes.remove( _name );
165    }
166  }
167
168  /**
169   * Return true if the name is valid.
170   *
171   * @param name the name to check.
172   * @return true if the name is valid.
173   */
174  private boolean isValidName( @Nonnull final String name )
175  {
176    if ( 0 == name.length() )
177    {
178      return false;
179    }
180    else
181    {
182      final int length = name.length();
183      for ( int i = 0; i < length; i++ )
184      {
185        final char ch = name.charAt( i );
186        if ( !Character.isLetterOrDigit( ch ) && '_' != ch && '-' != ch )
187        {
188          return false;
189        }
190      }
191      return true;
192    }
193  }
194}