001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.svl.functions;
018
019import java.util.*;
020
021import org.apache.juneau.commons.svl.*;
022
023/**
024 * Small collection helpers: {@code list} and {@code take}.
025 *
026 * <p>
027 * {@code list(a, b, ...)} builds a list from its arguments, which makes it possible to produce the rows for
028 * {@code table(header, rows)}, for example {@code rows.![list(a, b)]}.  {@code take(list, n)} returns the first {@code n}
029 * elements, which together with {@code size} expresses "first N, then 'and M more'" without a slice syntax.
030 *
031 * <p>
032 * Both functions are side-effect free.  Neither can grow a collection beyond its inputs: {@code list} is bounded by the
033 * number of arguments written in the template and {@code take} returns a prefix of its argument.
034 */
035public final class CollectionFunctions {
036
037   /**
038    * Applies the session's collection limit to a collection a function is about to copy.
039    *
040    * <p>
041    * Within the limit the elements are returned as a list.  Beyond it, strict mode throws and lenient mode keeps the
042    * first {@code maxCollectionSize} elements.
043    *
044    * @param session The session.
045    * @param fn The function name, for the message.
046    * @param c The collection.
047    * @return The elements, at most {@code maxCollectionSize} of them.
048    */
049   static List<Object> bounded(VarResolverSession session, String fn, Collection<?> c) {
050      var max = session.maxCollectionSize();
051      if (c.size() <= max)
052         return new ArrayList<>(c);
053      if (session.isStrict())
054         throw new VarResolverException("Function '%s' collection of %s elements exceeds the limit of %s", fn, c.size(), max);
055      var out = new ArrayList<Object>(max);
056      for (var o : c) {
057         if (out.size() >= max)
058            break;
059         out.add(o);
060      }
061      return out;
062   }
063
064   private CollectionFunctions() {}
065
066   /** All function classes in this category. */
067   @SuppressWarnings({
068      "java:S2386", // ALL is an immutable compile-time registry; exposed as an array for the cross-package/varargs functions(...) API.
069      "unchecked" // Cast is safe: type verified by caller context.
070   })
071   public static final Class<? extends VarFunction>[] ALL = new Class[] {
072      ListFn.class, Take.class
073   };
074
075   /** {@code #{list(a, b, ...)}} — a list of the arguments; {@code list()} is empty. */
076   public static class ListFn implements VarFunction {
077      @Override public String name() { return "list"; }
078      @Override public int minArity() { return 0; }
079      @Override public int maxArity() { return Integer.MAX_VALUE; }
080      @Override public String invoke(VarResolverSession session, List<Object> args) { return String.valueOf(invokeValue(session, args)); }
081      @Override public Object invokeValue(VarResolverSession session, List<Object> args) { return new ArrayList<>(args); }
082   }
083
084   /**
085    * {@code #{take(list, n)}} — the first {@code n} elements of {@code list} (all of them if it is shorter).
086    *
087    * <p>
088    * {@code n} must be a non-negative whole number; a {@code null} list yields an empty list.
089    */
090   // A range or slice syntax would supersede this if the SVL grammar ever adds one (see take()).
091   public static class Take implements VarFunction {
092      @Override public String name() { return "take"; }
093      @Override public int minArity() { return 2; }
094      @Override public int maxArity() { return 2; }
095      @Override public String invoke(VarResolverSession session, List<Object> args) { return String.valueOf(invokeValue(session, args)); }
096      @Override public Object invokeValue(VarResolverSession session, List<Object> args) {
097         var src = args.get(0);
098         var n = count(args.get(1));
099         if (src == null)
100            return new ArrayList<>();
101         if (! (src instanceof Collection<?> c))
102            throw new VarResolverException("Function 'take' argument 1 must be a list but got %s", src.getClass().getSimpleName());
103         var out = new ArrayList<Object>(Math.min(n, c.size()));
104         for (var o : bounded(session, "take", c)) {
105            if (out.size() >= n)
106               break;
107            out.add(o);
108         }
109         return out;
110      }
111
112      private static int count(Object o) {
113         if (o instanceof Number x && x.doubleValue() == Math.rint(x.doubleValue()) && x.doubleValue() >= 0 && x.doubleValue() <= Integer.MAX_VALUE)
114            return x.intValue();
115         if (o instanceof Number)
116            throw new VarResolverException("Function 'take' argument 2 must be a non-negative whole number but got %s", o);
117         throw new VarResolverException("Function 'take' argument 2 must be a number but got %s", o == null ? "null" : o.getClass().getSimpleName());
118      }
119   }
120}