22
33import java .util .ArrayList ;
44import java .util .HashMap ;
5+ import java .util .LinkedHashMap ;
6+ import java .util .LinkedHashSet ;
57import java .util .Iterator ;
68import java .util .List ;
79import java .util .Map ;
1517import io .teaql .core .utils .MapUtil ;
1618import io .teaql .core .utils .ObjectUtil ;
1719
18- public class SmartList <T extends Entity > implements Iterable <T > {
20+ public class SmartList <T > implements Iterable <T > {
1921 private static final Map <Class <?>, SmartList <?>> TYPED_EMPTY =
2022 new java .util .concurrent .ConcurrentHashMap <>();
23+ private static final SmartList <?> UNLOADED = new SmartList <>(true , false );
2124
2225 List <T > data ;
2326
@@ -26,7 +29,9 @@ public class SmartList<T extends Entity> implements Iterable<T> {
2629 @ FrameworkInternal ("Named page total metadata, independent of physical count SQL" )
2730 public void __internalSetPageCountAlias (String alias ) { ensureMutable (); pageCountAlias = alias ; }
2831
29- Map <String , SmartList > facets ;
32+ Map <String , SmartList <?>> facets ;
33+ private Map <String , Object > summary ;
34+ private boolean loaded = true ;
3035
3136 private final boolean immutable ;
3237
@@ -48,17 +53,24 @@ public SmartList(List<T> data) {
4853 }
4954 }
5055
51- private SmartList (boolean immutable ) {
56+ private SmartList (boolean immutable , boolean loaded ) {
5257 this .immutable = immutable ;
58+ this .loaded = loaded ;
5359 data = List .of ();
5460 }
5561
5662 @ SuppressWarnings ("unchecked" )
5763 public static <T extends Entity > SmartList <T > empty (Class <? extends T > entityType ) {
5864 if (entityType == null ) throw new IllegalArgumentException ("entityType must not be null" );
59- return (SmartList <T >) TYPED_EMPTY .computeIfAbsent (entityType , ignored -> new SmartList <>(true ));
65+ return (SmartList <T >) TYPED_EMPTY .computeIfAbsent (entityType , ignored -> new SmartList <>(true , true ));
6066 }
6167
68+ /** Unloaded relation marker; successful empty queries use a loaded empty list. */
69+ @ SuppressWarnings ("unchecked" )
70+ public static <T > SmartList <T > unloaded () { return (SmartList <T >) UNLOADED ; }
71+
72+ public boolean isLoaded () { return loaded ; }
73+
6274 @ FrameworkInternal
6375 public static <T extends Entity > SmartList <T > takeOwnership (List <T > data ) {
6476 SmartList <T > result = new SmartList <>();
@@ -93,7 +105,20 @@ public <R> Map<R, T> identityMap(Function<T, R> key) {
93105 }
94106
95107 public Map <Long , T > mapById () {
96- return identityMap (Entity ::getId );
108+ Map <Long , T > result = new LinkedHashMap <>();
109+ for (T item : data ) {
110+ Long id = entity (item ).getId ();
111+ if (id != null ) result .put (id , item );
112+ }
113+ return result ;
114+ }
115+
116+ public List <Long > ids () { return toList (item -> entity (item ).getId ()); }
117+ public List <Long > versions () { return toList (item -> entity (item ).getVersion ()); }
118+
119+ private static Entity entity (Object item ) {
120+ if (item instanceof Entity entity ) return entity ;
121+ throw new IllegalStateException ("Entity identity operations require entity rows" );
97122 }
98123
99124 public <R > Map <R , List <T >> groupBy (Function <T , R > key ) {
@@ -189,43 +214,55 @@ public T get(int index) {
189214 return data .get (index );
190215 }
191216
217+ public T getOrNull (int index ) { return index >= 0 && index < size () ? data .get (index ) : null ; }
218+ public T last () { return isEmpty () ? null : data .get (size () - 1 ); }
219+
192220 public SmartList <T > save (UserContext userContext ) {
193221 userContext .saveGraph (this );
194222 return this ;
195223 }
196224
197225
198- public int getTotalCount () {
199- if (ObjectUtil .isEmpty (aggregationResults )) {
200- return size ();
201- }
202- Map <String , Object > numberProps = aggregationNumberProperties ();
203- if (numberProps .isEmpty ()) {
204- return size ();
205- }
206- Object count = numberProps .get (pageCountAlias );
207- if (count instanceof Number intCount ) {
208- return intCount .intValue ();
226+ /** Exact source-query total, or null when no count was requested. */
227+ public Long getExactTotalCount () {
228+ Map <String , Object > properties = aggregationProperties ();
229+ if (!properties .containsKey (pageCountAlias )) return null ;
230+ Object count = properties .get (pageCountAlias );
231+ try {
232+ if (!(count instanceof Number )) throw new ArithmeticException ();
233+ long exact = new java .math .BigDecimal (count .toString ()).longValueExact ();
234+ if (exact < 0 ) throw new ArithmeticException ();
235+ return exact ;
236+ } catch (ArithmeticException | NumberFormatException invalid ) {
237+ throw new IllegalStateException ("List count must be an exact non-negative long" , invalid );
209238 }
210- throw new IllegalStateException ( "Number prop is expected a number, but it is now a " + count . getClass (). getSimpleName ());
239+ }
211240
212- //return aggregationResults.get(0).toInt();
241+ public long getTotalCountOrSize () {
242+ Long exact = getExactTotalCount ();
243+ return exact == null ? size () : exact ;
213244 }
214245
246+ /** @deprecated Use getExactTotalCount, or the explicitly named getTotalCountOrSize fallback. */
247+ @ Deprecated
248+ public int getTotalCount () { return Math .toIntExact (getTotalCountOrSize ()); }
249+
215250 public Map <String , Object > aggregationProperties (Class <?> clazz ) {
216251 if (ObjectUtil .isEmpty (aggregationResults )) {
217252 return MapUtil .empty ();
218253 }
219254 Map <String , Object > result = MapUtil .createMap (HashMap .class );
220255 getAggregationResults ().forEach (aggregationResult -> {
256+ if (aggregationResult .getData () == null ) return ;
221257
222258 //Map s=aggregationResult.toSimpleMap();
223259 List <Map <String , Object >> resultList = aggregationResult .valueList ();
224260
225261 resultList .forEach (map -> {
226262 map .entrySet ().forEach (stringObjectEntry -> {
227263
228- if (clazz .isAssignableFrom (stringObjectEntry .getValue ().getClass ())) {
264+ if (stringObjectEntry .getValue () == null ? clazz == Object .class
265+ : clazz .isInstance (stringObjectEntry .getValue ())) {
229266 result .put (stringObjectEntry .getKey (), stringObjectEntry .getValue ());
230267 }
231268
@@ -236,25 +273,25 @@ public Map<String, Object> aggregationProperties(Class<?> clazz) {
236273 return result ;
237274 }
238275
239- public void addFacet (String name , SmartList facet ) {
276+ public void addFacet (String name , SmartList <?> facet ) {
240277 ensureMutable ();
241278 mutableFacets ().put (name , facet );
242279 }
243280
244- public Map <String , SmartList > getFacets () {
281+ public Map <String , SmartList <?> > getFacets () {
245282 return mutableFacets ();
246283 }
247284
248- public void setFacets (Map <String , SmartList > facets ) {
285+ public void setFacets (Map <String , ? extends SmartList <?> > facets ) {
249286 ensureMutable ();
250- this .facets = facets ;
287+ this .facets = facets == null ? null : new LinkedHashMap <>( facets ) ;
251288 }
252289
253- public SmartList getFacet (String name ) {
290+ public SmartList <?> getFacet (String name ) {
254291 return facets == null ? null : facets .get (name );
255292 }
256293
257- public SmartList removeFacet (String name ) {
294+ public SmartList <?> removeFacet (String name ) {
258295 ensureMutable ();
259296 return facets == null ? null : facets .remove (name );
260297 }
@@ -264,15 +301,30 @@ public void clearFacets() {
264301 if (facets != null ) facets .clear ();
265302 }
266303
304+ public Map <String , SmartList <?>> takeFacets () {
305+ ensureMutable ();
306+ Map <String , SmartList <?>> result = facets == null ? new LinkedHashMap <>() : facets ;
307+ facets = null ;
308+ return result ;
309+ }
310+
311+ /** Summary population stays package-private; consumers receive a detached value tree. */
312+ void setSummary (Map <String , Object > values ) {
313+ ensureMutable ();
314+ summary = copyMap (values );
315+ }
316+
317+ public Map <String , Object > getSummary () { return copyMap (summary ); }
318+
267319 private List <AggregationResult > mutableAggregationResults () {
268320 if (immutable ) return List .of ();
269321 if (aggregationResults == null ) aggregationResults = new ArrayList <>();
270322 return aggregationResults ;
271323 }
272324
273- private Map <String , SmartList > mutableFacets () {
325+ private Map <String , SmartList <?> > mutableFacets () {
274326 if (immutable ) return Map .of ();
275- if (facets == null ) facets = new HashMap <>();
327+ if (facets == null ) facets = new LinkedHashMap <>();
276328 return facets ;
277329 }
278330
@@ -302,6 +354,85 @@ public boolean removeIf(Predicate<T> filter) {
302354 return data .removeIf (filter );
303355 }
304356
357+ /** One output row per source row, with an isolated query metadata snapshot. */
358+ public <R > SmartList <R > map (Function <? super T , ? extends R > mapper ) {
359+ SmartList <R > result = new SmartList <>(toList (mapper ::apply ));
360+ result .loaded = loaded ;
361+ result .pageCountAlias = pageCountAlias ;
362+ result .summary = copyMap (summary );
363+ if (aggregationResults != null ) {
364+ result .aggregationResults = new ArrayList <>();
365+ for (AggregationResult aggregation : aggregationResults ) {
366+ AggregationResult copy = new AggregationResult ();
367+ copy .setName (aggregation .getName ());
368+ List <AggregationItem > items = new ArrayList <>();
369+ if (aggregation .getData () != null ) for (AggregationItem item : aggregation .getData ()) {
370+ AggregationItem copied = new AggregationItem ();
371+ item .getValues ().forEach ((key , value ) -> copied .addValue (key , copyValue (value )));
372+ item .getDimensions ().forEach ((key , value ) -> copied .addDimension (key , copyValue (value )));
373+ items .add (copied );
374+ }
375+ copy .setData (items );
376+ result .aggregationResults .add (copy );
377+ }
378+ }
379+ if (facets != null ) {
380+ result .facets = new LinkedHashMap <>();
381+ facets .forEach ((name , facet ) -> result .facets .put (name , facet .map (SmartList ::copyValue )));
382+ }
383+ return result ;
384+ }
385+
386+ public SmartList <T > filter (Predicate <? super T > predicate ) {
387+ SmartList <T > result = new SmartList <>(data .stream ().filter (predicate ).toList ());
388+ result .loaded = loaded ;
389+ return result ;
390+ }
391+
392+ public <R > SmartList <R > flatMap (Function <? super T , ? extends Iterable <R >> mapper ) {
393+ SmartList <R > result = new SmartList <>();
394+ for (T item : data ) for (R value : mapper .apply (item )) result .add (value );
395+ result .loaded = loaded ;
396+ return result ;
397+ }
398+
399+ public <K > void mergeBy (Iterable <T > incoming , Function <? super T , K > key ) {
400+ ensureMutable ();
401+ Map <K , Integer > positions = new HashMap <>();
402+ for (int index = 0 ; index < size (); index ++) positions .put (key .apply (data .get (index )), index );
403+ for (T item : incoming ) {
404+ K itemKey = key .apply (item );
405+ Integer position = positions .get (itemKey );
406+ if (position != null ) set (position , item );
407+ else { positions .put (itemKey , size ()); add (item ); }
408+ }
409+ }
410+
411+ private static Map <String , Object > copyMap (Map <String , Object > values ) {
412+ Map <String , Object > result = new LinkedHashMap <>();
413+ if (values != null ) values .forEach ((key , value ) -> result .put (key , copyValue (value )));
414+ return result ;
415+ }
416+
417+ private static Object copyValue (Object value ) {
418+ if (value instanceof Map <?, ?> map ) {
419+ Map <Object , Object > copy = new LinkedHashMap <>();
420+ map .forEach ((key , item ) -> copy .put (key , copyValue (item )));
421+ return copy ;
422+ }
423+ if (value instanceof List <?> list ) {
424+ List <Object > copy = new ArrayList <>();
425+ list .forEach (item -> copy .add (copyValue (item )));
426+ return copy ;
427+ }
428+ if (value instanceof Set <?> set ) {
429+ Set <Object > copy = new LinkedHashSet <>();
430+ set .forEach (item -> copy .add (copyValue (item )));
431+ return copy ;
432+ }
433+ return value ;
434+ }
435+
305436 private void ensureMutable () {
306437 if (immutable ) {
307438 throw new UnsupportedOperationException ("A shared empty SmartList is immutable" );
0 commit comments