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}