All Downloads are FREE. Search and download functionalities are using the official Maven repository.

arez.Component Maven / Gradle / Ivy

There is a newer version: 0.213
Show newest version
package arez;

import arez.spy.ComponentCreateCompleteEvent;
import arez.spy.ComponentDisposeCompleteEvent;
import arez.spy.ComponentDisposeStartEvent;
import arez.spy.ComponentInfo;
import grim.annotations.OmitSymbol;
import grim.annotations.OmitType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.realityforge.braincheck.Guards.*;

/**
 * The component is an abstraction representation of a reactive component within Arez.
 * Each component is made up of one or more of the core Arez reactive elements: {@link ObservableValue}s,
 * {@link Observer}s or {@link ComputableValue}s.
 */
@OmitType( unless = "arez.enable_native_components" )
public final class Component
  implements Disposable
{
  /**
   * Reference to the system to which this node belongs.
   */
  @OmitSymbol( unless = "arez.enable_zones" )
  @Nullable
  private final ArezContext _context;
  /**
   * A opaque string describing the type of the component.
   * It corresponds to @ArezComponent.name parameter if this component was built using the annotation processor.
   */
  @Nonnull
  private final String _type;
  /**
   * The id of the component.
   */
  @Nonnull
  private final Object _id;
  /**
   * A human consumable name for node. It should be non-null if {@link Arez#areNamesEnabled()} returns
   * true and null otherwise.
   */
  @Nullable
  @OmitSymbol( unless = "arez.enable_names" )
  private final String _name;
  @Nonnull
  private final List> _observableValues = new ArrayList<>();
  @Nonnull
  private final List _observers = new ArrayList<>();
  @Nonnull
  private final List> _computableValues = new ArrayList<>();
  /**
   * Hook action called just before the Component is disposed.
   * Occurs inside the dispose transaction.
   */
  @Nullable
  private final SafeProcedure _preDispose;
  /**
   * Hook action called just after the Component is disposed.
   */
  @Nullable
  private final SafeProcedure _postDispose;
  private boolean _complete;
  private boolean _disposed;
  /**
   * Cached info object associated with element.
   * This should be null if {@link Arez#areSpiesEnabled()} is false;
   */
  @OmitSymbol( unless = "arez.enable_spies" )
  @Nullable
  private ComponentInfo _info;

  Component( @Nullable final ArezContext context,
             @Nonnull final String type,
             @Nonnull final Object id,
             @Nullable final String name,
             @Nullable final SafeProcedure preDispose,
             @Nullable final SafeProcedure postDispose )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> Arez.areNamesEnabled() || null == name,
                    () -> "Arez-0037: Component passed a name '" + name + "' but Arez.areNamesEnabled() is false" );
      invariant( () -> Arez.areZonesEnabled() || null == context,
                 () -> "Arez-0175: Component passed a context but Arez.areZonesEnabled() is false" );
    }
    _context = Arez.areZonesEnabled() ? Objects.requireNonNull( context ) : null;
    _type = Objects.requireNonNull( type );
    _id = Objects.requireNonNull( id );
    _name = Arez.areNamesEnabled() ? Objects.requireNonNull( name ) : null;
    _preDispose = preDispose;
    _postDispose = postDispose;
  }

  /**
   * Return the component type.
   * This is an opaque string specified by the user.
   *
   * @return the component type.
   */
  @Nonnull
  public String getType()
  {
    return _type;
  }

  /**
   * Return the unique id of the component.
   * This will return null for singletons.
   *
   * @return the unique id of the component.
   */
  @Nonnull
  public Object getId()
  {
    return _id;
  }

  /**
   * Return the unique name of the component.
   *
   * @return the name of the component.
   */
  @Nonnull
  public String getName()
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( Arez::areNamesEnabled,
                    () -> "Arez-0038: Component.getName() invoked when Arez.areNamesEnabled() is false" );
    }
    assert null != _name;
    return _name;
  }

  @Nonnull
  ArezContext getContext()
  {
    return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context();
  }

  @Override
  public void dispose()
  {
    if ( !_disposed )
    {
      _disposed = true;
      if ( Arez.areSpiesEnabled() && getContext().getSpy().willPropagateSpyEvents() )
      {
        final ComponentInfo info = getContext().getSpy().asComponentInfo( this );
        getContext().getSpy().reportSpyEvent( new ComponentDisposeStartEvent( info ) );
      }
      getContext().safeAction( Arez.areNamesEnabled() ? getName() + ".dispose" : null, () -> {
        if ( null != _preDispose )
        {
          _preDispose.call();
        }
        getContext().deregisterComponent( this );
        /*
         * Create a new list and perform dispose on each list to avoid concurrent mutation exceptions.
         * This can probably be significantly optimized when translated to javascript. However native
         * components are not typically used in production mode so no effort has been made to optimize
         * the next steps.
         */
        new ArrayList<>( _observers ).forEach( o -> Disposable.dispose( o ) );
        new ArrayList<>( _computableValues ).forEach( v -> Disposable.dispose( v ) );
        new ArrayList<>( _observableValues ).forEach( o -> Disposable.dispose( o ) );
        if ( null != _postDispose )
        {
          _postDispose.call();
        }
      }, ActionFlags.NO_VERIFY_ACTION_REQUIRED );
      if ( Arez.areSpiesEnabled() && getContext().getSpy().willPropagateSpyEvents() )
      {
        final ComponentInfo info = getContext().getSpy().asComponentInfo( this );
        getContext().getSpy().reportSpyEvent( new ComponentDisposeCompleteEvent( info ) );
      }
    }
  }

  @Override
  public boolean isDisposed()
  {
    return _disposed;
  }

  @Nonnull
  @Override
  public String toString()
  {
    if ( Arez.areNamesEnabled() )
    {
      return getName();
    }
    else
    {
      return super.toString();
    }
  }

  /**
   * Return true if the creation of this component is complete.
   *
   * @return true if the creation of this component is complete, false otherwise.
   */
  public boolean isComplete()
  {
    return _complete;
  }

  /**
   * The toolkit user should call this method when the component is complete.
   * After this method has been invoked the user should not attempt to define any more {@link ObservableValue}s,
   * {@link Observer}s or {@link ComputableValue}s on the component.
   */
  public void complete()
  {
    if ( !_complete )
    {
      _complete = true;
      if ( Arez.areSpiesEnabled() && getContext().getSpy().willPropagateSpyEvents() )
      {
        final ComponentInfo component = getContext().getSpy().asComponentInfo( this );
        getContext().getSpy().reportSpyEvent( new ComponentCreateCompleteEvent( component ) );
      }
    }
  }

  /**
   * Return the info associated with this class.
   *
   * @return the info associated with this class.
   */
  @SuppressWarnings( "ConstantConditions" )
  @OmitSymbol( unless = "arez.enable_spies" )
  @Nonnull
  ComponentInfo asInfo()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( Arez::areSpiesEnabled,
                 () -> "Arez-0194: Component.asInfo() invoked but Arez.areSpiesEnabled() returned false." );
    }
    if ( Arez.areSpiesEnabled() && null == _info )
    {
      _info = new ComponentInfoImpl( this );
    }
    return Arez.areSpiesEnabled() ? _info : null;
  }

  /**
   * Return the observers associated with the component.
   *
   * @return the observers associated with the component.
   */
  @Nonnull
  List getObservers()
  {
    return _observers;
  }

  /**
   * Add observer to component.
   * Observer should not be part of observer.
   *
   * @param observer the observer.
   */
  void addObserver( @Nonnull final Observer observer )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> !_observers.contains( observer ),
                    () -> "Arez-0040: Component.addObserver invoked on component '" + getName() + "' specifying " +
                          "observer named '" + observer.getName() + "' when observer already exists for component." );
    }
    _observers.add( observer );
  }

  /**
   * Remove observer from the component.
   * Observer should be part of component.
   *
   * @param observer the observer.
   */
  void removeObserver( @Nonnull final Observer observer )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> _observers.contains( observer ),
                    () -> "Arez-0041: Component.removeObserver invoked on component '" + getName() + "' specifying " +
                          "observer named '" + observer.getName() + "' when observer does not exist for component." );
    }
    _observers.remove( observer );
  }

  /**
   * Return the observables associated with the component.
   *
   * @return the observables associated with the component.
   */
  @Nonnull
  List> getObservableValues()
  {
    return _observableValues;
  }

  /**
   * Add observableValue to component.
   * ObservableValue should not be part of component.
   *
   * @param observableValue the observableValue.
   */
  void addObservableValue( @Nonnull final ObservableValue observableValue )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> !_complete,
                    () -> "Arez-0042: Component.addObservableValue invoked on component '" +
                          getName() +
                          "' " +
                          "specifying ObservableValue named '" +
                          observableValue.getName() +
                          "' when component.complete() " +
                          "has already been called." );
      apiInvariant( () -> !_observableValues.contains( observableValue ),
                    () -> "Arez-0043: Component.addObservableValue invoked on component '" +
                          getName() +
                          "' " +
                          "specifying ObservableValue named '" +
                          observableValue.getName() +
                          "' when ObservableValue already " +
                          "exists for component." );
    }
    _observableValues.add( observableValue );
  }

  /**
   * Remove observableValue from the component.
   * ObservableValue should be part of component.
   *
   * @param observableValue the observableValue.
   */
  void removeObservableValue( @Nonnull final ObservableValue observableValue )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> _observableValues.contains( observableValue ),
                    () -> "Arez-0044: Component.removeObservableValue invoked on component '" +
                          getName() +
                          "' " +
                          "specifying ObservableValue named '" +
                          observableValue.getName() +
                          "' when ObservableValue does not " +
                          "exist for component." );
    }
    _observableValues.remove( observableValue );
  }

  /**
   * Return the {@link ComputableValue} instances associated with the component.
   *
   * @return the {@link ComputableValue} instances associated with the component.
   */
  @Nonnull
  List> getComputableValues()
  {
    return _computableValues;
  }

  /**
   * Add computableValue to component.
   * ComputableValue should not be part of component.
   *
   * @param computableValue the computableValue.
   */
  void addComputableValue( @Nonnull final ComputableValue computableValue )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> !_computableValues.contains( computableValue ),
                    () -> "Arez-0046: Component.addComputableValue invoked on component '" + getName() + "' " +
                          "specifying ComputableValue named '" + computableValue.getName() + "' when " +
                          "ComputableValue already exists for component." );
    }
    _computableValues.add( computableValue );
  }

  /**
   * Remove computableValue from the component.
   * ComputableValue should be part of component.
   *
   * @param computableValue the computableValue.
   */
  void removeComputableValue( @Nonnull final ComputableValue computableValue )
  {
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> _computableValues.contains( computableValue ),
                    () -> "Arez-0047: Component.removeComputableValue invoked on component '" + getName() + "' " +
                          "specifying ComputableValue named '" + computableValue.getName() + "' when " +
                          "ComputableValue does not exist for component." );
    }
    _computableValues.remove( computableValue );
  }

  @OmitSymbol
  @Nullable
  SafeProcedure getPreDispose()
  {
    return _preDispose;
  }

  @OmitSymbol
  @Nullable
  SafeProcedure getPostDispose()
  {
    return _postDispose;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy