001package arez; 002 003import arez.spy.ComponentCreateCompleteEvent; 004import arez.spy.ComponentDisposeCompleteEvent; 005import arez.spy.ComponentDisposeStartEvent; 006import arez.spy.ComponentInfo; 007import grim.annotations.OmitSymbol; 008import grim.annotations.OmitType; 009import java.util.ArrayList; 010import java.util.List; 011import java.util.Objects; 012import javax.annotation.Nonnull; 013import javax.annotation.Nullable; 014import static org.realityforge.braincheck.Guards.*; 015 016/** 017 * The component is an abstraction representation of a reactive component within Arez. 018 * Each component is made up of one or more of the core Arez reactive elements: {@link ObservableValue}s, 019 * {@link Observer}s or {@link ComputableValue}s. 020 */ 021@OmitType( unless = "arez.enable_native_components" ) 022public final class Component 023 implements Disposable 024{ 025 /** 026 * Reference to the system to which this node belongs. 027 */ 028 @OmitSymbol( unless = "arez.enable_zones" ) 029 @Nullable 030 private final ArezContext _context; 031 /** 032 * A opaque string describing the type of the component. 033 * It corresponds to @ArezComponent.name parameter if this component was built using the annotation processor. 034 */ 035 @Nonnull 036 private final String _type; 037 /** 038 * The id of the component. 039 */ 040 @Nonnull 041 private final Object _id; 042 /** 043 * A human consumable name for node. It should be non-null if {@link Arez#areNamesEnabled()} returns 044 * true and <code>null</code> otherwise. 045 */ 046 @Nullable 047 @OmitSymbol( unless = "arez.enable_names" ) 048 private final String _name; 049 @Nonnull 050 private final List<ObservableValue<?>> _observableValues = new ArrayList<>(); 051 @Nonnull 052 private final List<Observer> _observers = new ArrayList<>(); 053 @Nonnull 054 private final List<ComputableValue<?>> _computableValues = new ArrayList<>(); 055 /** 056 * Hook action called just before the Component is disposed. 057 * Occurs inside the dispose transaction. 058 */ 059 @Nullable 060 private final SafeProcedure _preDispose; 061 /** 062 * Hook action called just after the Component is disposed. 063 */ 064 @Nullable 065 private final SafeProcedure _postDispose; 066 private boolean _complete; 067 private boolean _disposed; 068 /** 069 * Cached info object associated with element. 070 * This should be null if {@link Arez#areSpiesEnabled()} is false; 071 */ 072 @OmitSymbol( unless = "arez.enable_spies" ) 073 @Nullable 074 private ComponentInfo _info; 075 076 Component( @Nullable final ArezContext context, 077 @Nonnull final String type, 078 @Nonnull final Object id, 079 @Nullable final String name, 080 @Nullable final SafeProcedure preDispose, 081 @Nullable final SafeProcedure postDispose ) 082 { 083 if ( Arez.shouldCheckApiInvariants() ) 084 { 085 apiInvariant( () -> Arez.areNamesEnabled() || null == name, 086 () -> "Arez-0037: Component passed a name '" + name + "' but Arez.areNamesEnabled() is false" ); 087 invariant( () -> Arez.areZonesEnabled() || null == context, 088 () -> "Arez-0175: Component passed a context but Arez.areZonesEnabled() is false" ); 089 } 090 _context = Arez.areZonesEnabled() ? Objects.requireNonNull( context ) : null; 091 _type = Objects.requireNonNull( type ); 092 _id = Objects.requireNonNull( id ); 093 _name = Arez.areNamesEnabled() ? Objects.requireNonNull( name ) : null; 094 _preDispose = preDispose; 095 _postDispose = postDispose; 096 } 097 098 /** 099 * Return the component type. 100 * This is an opaque string specified by the user. 101 * 102 * @return the component type. 103 */ 104 @Nonnull 105 public String getType() 106 { 107 return _type; 108 } 109 110 /** 111 * Return the unique id of the component. 112 * This will return null for singletons. 113 * 114 * @return the unique id of the component. 115 */ 116 @Nonnull 117 public Object getId() 118 { 119 return _id; 120 } 121 122 /** 123 * Return the unique name of the component. 124 * 125 * @return the name of the component. 126 */ 127 @Nonnull 128 public String getName() 129 { 130 if ( Arez.shouldCheckApiInvariants() ) 131 { 132 apiInvariant( Arez::areNamesEnabled, 133 () -> "Arez-0038: Component.getName() invoked when Arez.areNamesEnabled() is false" ); 134 } 135 assert null != _name; 136 return _name; 137 } 138 139 @Nonnull 140 ArezContext getContext() 141 { 142 return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context(); 143 } 144 145 @Override 146 public void dispose() 147 { 148 if ( !_disposed ) 149 { 150 _disposed = true; 151 if ( Arez.areSpiesEnabled() && getContext().getSpy().willPropagateSpyEvents() ) 152 { 153 final ComponentInfo info = getContext().getSpy().asComponentInfo( this ); 154 getContext().getSpy().reportSpyEvent( new ComponentDisposeStartEvent( info ) ); 155 } 156 getContext().safeAction( Arez.areNamesEnabled() ? getName() + ".dispose" : null, () -> { 157 if ( null != _preDispose ) 158 { 159 _preDispose.call(); 160 } 161 getContext().deregisterComponent( this ); 162 /* 163 * Create a new list and perform dispose on each list to avoid concurrent mutation exceptions. 164 * This can probably be significantly optimized when translated to javascript. However native 165 * components are not typically used in production mode so no effort has been made to optimize 166 * the next steps. 167 */ 168 new ArrayList<>( _observers ).forEach( o -> Disposable.dispose( o ) ); 169 new ArrayList<>( _computableValues ).forEach( v -> Disposable.dispose( v ) ); 170 new ArrayList<>( _observableValues ).forEach( o -> Disposable.dispose( o ) ); 171 if ( null != _postDispose ) 172 { 173 _postDispose.call(); 174 } 175 }, ActionFlags.NO_VERIFY_ACTION_REQUIRED ); 176 if ( Arez.areSpiesEnabled() && getContext().getSpy().willPropagateSpyEvents() ) 177 { 178 final ComponentInfo info = getContext().getSpy().asComponentInfo( this ); 179 getContext().getSpy().reportSpyEvent( new ComponentDisposeCompleteEvent( info ) ); 180 } 181 } 182 } 183 184 @Override 185 public boolean isDisposed() 186 { 187 return _disposed; 188 } 189 190 @Nonnull 191 @Override 192 public String toString() 193 { 194 if ( Arez.areNamesEnabled() ) 195 { 196 return getName(); 197 } 198 else 199 { 200 return super.toString(); 201 } 202 } 203 204 /** 205 * Return true if the creation of this component is complete. 206 * 207 * @return true if the creation of this component is complete, false otherwise. 208 */ 209 public boolean isComplete() 210 { 211 return _complete; 212 } 213 214 /** 215 * The toolkit user should call this method when the component is complete. 216 * After this method has been invoked the user should not attempt to define any more {@link ObservableValue}s, 217 * {@link Observer}s or {@link ComputableValue}s on the component. 218 */ 219 public void complete() 220 { 221 if ( !_complete ) 222 { 223 _complete = true; 224 if ( Arez.areSpiesEnabled() && getContext().getSpy().willPropagateSpyEvents() ) 225 { 226 final ComponentInfo component = getContext().getSpy().asComponentInfo( this ); 227 getContext().getSpy().reportSpyEvent( new ComponentCreateCompleteEvent( component ) ); 228 } 229 } 230 } 231 232 /** 233 * Return the info associated with this class. 234 * 235 * @return the info associated with this class. 236 */ 237 @SuppressWarnings( "ConstantConditions" ) 238 @OmitSymbol( unless = "arez.enable_spies" ) 239 @Nonnull 240 ComponentInfo asInfo() 241 { 242 if ( Arez.shouldCheckInvariants() ) 243 { 244 invariant( Arez::areSpiesEnabled, 245 () -> "Arez-0194: Component.asInfo() invoked but Arez.areSpiesEnabled() returned false." ); 246 } 247 if ( Arez.areSpiesEnabled() && null == _info ) 248 { 249 _info = new ComponentInfoImpl( this ); 250 } 251 return Arez.areSpiesEnabled() ? _info : null; 252 } 253 254 /** 255 * Return the observers associated with the component. 256 * 257 * @return the observers associated with the component. 258 */ 259 @Nonnull 260 List<Observer> getObservers() 261 { 262 return _observers; 263 } 264 265 /** 266 * Add observer to component. 267 * Observer should not be part of observer. 268 * 269 * @param observer the observer. 270 */ 271 void addObserver( @Nonnull final Observer observer ) 272 { 273 if ( Arez.shouldCheckApiInvariants() ) 274 { 275 apiInvariant( () -> !_observers.contains( observer ), 276 () -> "Arez-0040: Component.addObserver invoked on component '" + getName() + "' specifying " + 277 "observer named '" + observer.getName() + "' when observer already exists for component." ); 278 } 279 _observers.add( observer ); 280 } 281 282 /** 283 * Remove observer from the component. 284 * Observer should be part of component. 285 * 286 * @param observer the observer. 287 */ 288 void removeObserver( @Nonnull final Observer observer ) 289 { 290 if ( Arez.shouldCheckApiInvariants() ) 291 { 292 apiInvariant( () -> _observers.contains( observer ), 293 () -> "Arez-0041: Component.removeObserver invoked on component '" + getName() + "' specifying " + 294 "observer named '" + observer.getName() + "' when observer does not exist for component." ); 295 } 296 _observers.remove( observer ); 297 } 298 299 /** 300 * Return the observables associated with the component. 301 * 302 * @return the observables associated with the component. 303 */ 304 @Nonnull 305 List<ObservableValue<?>> getObservableValues() 306 { 307 return _observableValues; 308 } 309 310 /** 311 * Add observableValue to component. 312 * ObservableValue should not be part of component. 313 * 314 * @param observableValue the observableValue. 315 */ 316 void addObservableValue( @Nonnull final ObservableValue<?> observableValue ) 317 { 318 if ( Arez.shouldCheckApiInvariants() ) 319 { 320 apiInvariant( () -> !_complete, 321 () -> "Arez-0042: Component.addObservableValue invoked on component '" + 322 getName() + 323 "' " + 324 "specifying ObservableValue named '" + 325 observableValue.getName() + 326 "' when component.complete() " + 327 "has already been called." ); 328 apiInvariant( () -> !_observableValues.contains( observableValue ), 329 () -> "Arez-0043: Component.addObservableValue invoked on component '" + 330 getName() + 331 "' " + 332 "specifying ObservableValue named '" + 333 observableValue.getName() + 334 "' when ObservableValue already " + 335 "exists for component." ); 336 } 337 _observableValues.add( observableValue ); 338 } 339 340 /** 341 * Remove observableValue from the component. 342 * ObservableValue should be part of component. 343 * 344 * @param observableValue the observableValue. 345 */ 346 void removeObservableValue( @Nonnull final ObservableValue<?> observableValue ) 347 { 348 if ( Arez.shouldCheckApiInvariants() ) 349 { 350 apiInvariant( () -> _observableValues.contains( observableValue ), 351 () -> "Arez-0044: Component.removeObservableValue invoked on component '" + 352 getName() + 353 "' " + 354 "specifying ObservableValue named '" + 355 observableValue.getName() + 356 "' when ObservableValue does not " + 357 "exist for component." ); 358 } 359 _observableValues.remove( observableValue ); 360 } 361 362 /** 363 * Return the {@link ComputableValue} instances associated with the component. 364 * 365 * @return the {@link ComputableValue} instances associated with the component. 366 */ 367 @Nonnull 368 List<ComputableValue<?>> getComputableValues() 369 { 370 return _computableValues; 371 } 372 373 /** 374 * Add computableValue to component. 375 * ComputableValue should not be part of component. 376 * 377 * @param computableValue the computableValue. 378 */ 379 void addComputableValue( @Nonnull final ComputableValue<?> computableValue ) 380 { 381 if ( Arez.shouldCheckApiInvariants() ) 382 { 383 apiInvariant( () -> !_computableValues.contains( computableValue ), 384 () -> "Arez-0046: Component.addComputableValue invoked on component '" + getName() + "' " + 385 "specifying ComputableValue named '" + computableValue.getName() + "' when " + 386 "ComputableValue already exists for component." ); 387 } 388 _computableValues.add( computableValue ); 389 } 390 391 /** 392 * Remove computableValue from the component. 393 * ComputableValue should be part of component. 394 * 395 * @param computableValue the computableValue. 396 */ 397 void removeComputableValue( @Nonnull final ComputableValue<?> computableValue ) 398 { 399 if ( Arez.shouldCheckApiInvariants() ) 400 { 401 apiInvariant( () -> _computableValues.contains( computableValue ), 402 () -> "Arez-0047: Component.removeComputableValue invoked on component '" + getName() + "' " + 403 "specifying ComputableValue named '" + computableValue.getName() + "' when " + 404 "ComputableValue does not exist for component." ); 405 } 406 _computableValues.remove( computableValue ); 407 } 408 409 @OmitSymbol 410 @Nullable 411 SafeProcedure getPreDispose() 412 { 413 return _preDispose; 414 } 415 416 @OmitSymbol 417 @Nullable 418 SafeProcedure getPostDispose() 419 { 420 return _postDispose; 421 } 422}