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}