001package arez.component.internal;
002
003import arez.Arez;
004import arez.Disposable;
005import arez.ObservableValue;
006import arez.annotations.Observable;
007import arez.annotations.ObservableValueRef;
008import arez.annotations.PreDispose;
009import arez.component.ComponentObservable;
010import arez.component.DisposeNotifier;
011import arez.component.Identifiable;
012import arez.component.NoResultException;
013import arez.component.NoSuchEntityException;
014import java.util.Comparator;
015import java.util.HashMap;
016import java.util.List;
017import java.util.Map;
018import java.util.function.Predicate;
019import java.util.stream.Stream;
020import javax.annotation.Nonnull;
021import javax.annotation.Nullable;
022import static org.realityforge.braincheck.Guards.*;
023
024/**
025 * Abstract base class for repositories that contain Arez components.
026 * This class is used by the annotation processor as a base class from which to derive the actual
027 * repositories for each type.
028 *
029 * <p>When multiple results are returned as a list, they are passed through {@link CollectionsUtil#asList(Stream)} or
030 * {@link CollectionsUtil#wrap(List)} and this will convert the result set to an unmodifiable variant if
031 * {@link Arez#areCollectionsPropertiesUnmodifiable()} returns true. Typically this means that in
032 * development mode these will be made immutable but that the lists will be passed through as-is
033 * in production mode for maximum performance.</p>
034 */
035public abstract class AbstractRepository<K, T, R extends AbstractRepository<K, T, R>>
036{
037  /**
038   * A map of all the entities ArezId to entity.
039   */
040  @Nonnull
041  private final Map<K, T> _entities = new HashMap<>();
042
043  protected final boolean shouldDisposeEntryOnDispose()
044  {
045    return true;
046  }
047
048  public boolean contains( @Nonnull final T entity )
049  {
050    if ( reportRead() )
051    {
052      getEntitiesObservableValue().reportObserved();
053    }
054    return _entities.containsKey( Identifiable.<K>getArezId( entity ) );
055  }
056
057  /**
058   * Return all the entities.
059   *
060   * @return all the entities.
061   */
062  @Nonnull
063  public final List<T> findAll()
064  {
065    return CollectionsUtil.asList( entities() );
066  }
067
068  /**
069   * Return all entities sorted by supplied comparator.
070   *
071   * @param sorter the comparator used to sort entities.
072   * @return the entity list result.
073   */
074  @Nonnull
075  public final List<T> findAll( @Nonnull final Comparator<T> sorter )
076  {
077    return CollectionsUtil.asList( entities().sorted( sorter ) );
078  }
079
080  /**
081   * Return all entities that match query.
082   *
083   * @param query the predicate used to select entities.
084   * @return the entity list result.
085   */
086  @Nonnull
087  public final List<T> findAllByQuery( @Nonnull final Predicate<T> query )
088  {
089    return CollectionsUtil.asList( entities().filter( query ) );
090  }
091
092  /**
093   * Return all entities that match query sorted by supplied comparator.
094   *
095   * @param query  the predicate used to select entities.
096   * @param sorter the comparator used to sort entities.
097   * @return the entity list result.
098   */
099  @Nonnull
100  public final List<T> findAllByQuery( @Nonnull final Predicate<T> query, @Nonnull final Comparator<T> sorter )
101  {
102    return CollectionsUtil.asList( entities().filter( query ).sorted( sorter ) );
103  }
104
105  /**
106   * Return the entity that matches query or null if unable to locate matching entity.
107   *
108   * @param query the predicate used to select entity.
109   * @return the entity or null if unable to locate matching entity.
110   */
111  @Nullable
112  public final T findByQuery( @Nonnull final Predicate<T> query )
113  {
114    return entities().filter( query ).findFirst().orElse( null );
115  }
116
117  /**
118   * Return the entity that matches query else throw an exception.
119   *
120   * @param query the predicate used to select entity.
121   * @return the entity.
122   * @throws NoResultException if unable to locate matching entity.
123   */
124  @Nonnull
125  public final T getByQuery( @Nonnull final Predicate<T> query )
126    throws NoResultException
127  {
128    final T entity = findByQuery( query );
129    if ( null == entity )
130    {
131      throw new NoResultException();
132    }
133    return entity;
134  }
135
136  @Nullable
137  public final T findByArezId( @Nonnull final K arezId )
138  {
139    final T entity = _entities.get( arezId );
140    if ( null != entity )
141    {
142      if ( reportRead() )
143      {
144        ComponentObservable.observe( entity );
145      }
146      return entity;
147    }
148    if ( reportRead() )
149    {
150      getEntitiesObservableValue().reportObserved();
151    }
152    return null;
153  }
154
155  @Nonnull
156  public final T getByArezId( @Nonnull final K arezId )
157    throws NoSuchEntityException
158  {
159    final T entity = findByArezId( arezId );
160    if ( null == entity )
161    {
162      throw new NoSuchEntityException( arezId );
163    }
164    return entity;
165  }
166
167  /**
168   * Return the repository instance cast to typed subtype.
169   *
170   * @return the repository instance.
171   */
172  @SuppressWarnings( "unchecked" )
173  @Nonnull
174  public final R self()
175  {
176    return (R) this;
177  }
178
179  /**
180   * Attach specified entity to the set of entities managed by the container.
181   * This should not be invoked if the entity is already attached to the repository.
182   *
183   * @param entity the entity to register.
184   */
185  @SuppressWarnings( "SuspiciousMethodCalls" )
186  protected void attach( @Nonnull final T entity )
187  {
188    if ( Arez.shouldCheckApiInvariants() )
189    {
190      apiInvariant( () -> Disposable.isNotDisposed( entity ),
191                    () -> "Arez-0168: Called attach() passing an entity that is disposed. Entity: " + entity );
192      apiInvariant( () -> !_entities.containsKey( Identifiable.getArezId( entity ) ),
193                    () -> "Arez-0136: Called attach() passing an entity that is already attached " +
194                          "to the container. Entity: " + entity );
195    }
196    getEntitiesObservableValue().preReportChanged();
197    attachEntity( entity );
198    _entities.put( Identifiable.getArezId( entity ), entity );
199    getEntitiesObservableValue().reportChanged();
200  }
201
202  /**
203   * Dispose or detach all the entities associated with the container.
204   */
205  @PreDispose
206  protected void preDispose()
207  {
208    _entities.values().forEach( entry -> detachEntity( entry, shouldDisposeEntryOnDispose() ) );
209    _entities.clear();
210  }
211
212  /**
213   * Detach the entity from the container and dispose the entity.
214   * The entity must be attached to the container.
215   *
216   * @param entity the entity to destroy.
217   */
218  protected void destroy( @Nonnull final T entity )
219  {
220    detach( entity, true );
221  }
222
223  /**
224   * Detach entity from container without disposing entity.
225   * The entity must be attached to the container.
226   *
227   * @param entity the entity to detach.
228   */
229  protected void detach( @Nonnull final T entity )
230  {
231    detach( entity, false );
232  }
233
234  /**
235   * Detach entity from container without disposing entity.
236   * The entity must be attached to the container.
237   *
238   * @param entity the entity to detach.
239   */
240  private void detach( @Nonnull final T entity, final boolean disposeEntity )
241  {
242    // This method has been extracted to try and avoid GWT inlining into invoker
243    final T removed = _entities.remove( Identifiable.<K>getArezId( entity ) );
244    if ( null != removed )
245    {
246      getEntitiesObservableValue().preReportChanged();
247      detachEntity( entity, disposeEntity );
248      getEntitiesObservableValue().reportChanged();
249    }
250    else
251    {
252      fail( () -> "Arez-0157: Called detach() passing an entity that was not attached to the container. Entity: " +
253                  entity );
254    }
255  }
256
257  protected boolean reportRead()
258  {
259    return true;
260  }
261
262  /**
263   * Return the observable associated with entities.
264   * This template method is implemented by the Arez annotation processor and is used internally
265   * to container. It should not be invoked by extensions.
266   *
267   * @return the Arez observable associated with entities observable property.
268   */
269  @ObservableValueRef
270  @Nonnull
271  protected abstract ObservableValue<?> getEntitiesObservableValue();
272
273  /**
274   * Return a stream of all entities in the container.
275   *
276   * @return the underlying entities.
277   */
278  @Nonnull
279  public Stream<T> entities()
280  {
281    if ( reportRead() )
282    {
283      getEntitiesObservableValue().reportObserved();
284    }
285    return entityStream();
286  }
287
288  /**
289   * Return a stream of all entities in the container.
290   *
291   * @return the underlying entities.
292   */
293  @Observable( name = "entities", expectSetter = false )
294  @Nonnull
295  protected Stream<T> entitiesValue()
296  {
297    return entityStream();
298  }
299
300  @Nonnull
301  private Stream<T> entityStream()
302  {
303    return _entities.values().stream();
304  }
305
306  private void attachEntity( @Nonnull final T entity )
307  {
308    DisposeNotifier
309      .asDisposeNotifier( entity )
310      .addOnDisposeListener( this, () -> {
311        getEntitiesObservableValue().preReportChanged();
312        detach( entity, false );
313        getEntitiesObservableValue().reportChanged();
314      } );
315  }
316
317  private void detachEntity( @Nonnull final T entity, final boolean disposeOnDetach )
318  {
319    DisposeNotifier.asDisposeNotifier( entity ).removeOnDisposeListener( this );
320    if ( disposeOnDetach )
321    {
322      Disposable.dispose( entity );
323    }
324  }
325}