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}