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.console.freemarker;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.marshall.collections.*;
024import org.apache.juneau.console.*;
025
026/**
027 * Declares a derived data source: transforms run in the browser on each new frame of another source, published on
028 * <c>data:&lt;as&gt;</c>.
029 *
030 * <p>
031 * The built-in transforms are <c>bucket</c>, <c>filter</c>, <c>groupBy</c> and <c>topN</c>; app transforms are
032 * registered under dotted names.  A string arg that is exactly <c>{token}</c> is filled from params.
033 *
034 * <h5 class='section'>Example:</h5>
035 * <p class='bjava'>
036 *    <jv>page</jv>.data(Derived.<jsm>from</jsm>(<js>"orgRequests"</js>)
037 *       .transform(<js>"groupBy"</js>, JsonMap.<jsm>of</jsm>(<js>"by"</js>, List.<jsm>of</jsm>(<js>"reason"</js>),
038 *          <js>"measures"</js>, List.<jsm>of</jsm>(<js>"count"</js>)))
039 *       .as(<js>"noqcByReason"</js>));
040 * </p>
041 *
042 * @since 10.0.0
043 */
044public final class Derived {
045
046   private final String from;
047   private String id;
048   private final List<JsonMap> transforms = new ArrayList<>();
049   private final List<Subscription> params = new ArrayList<>();
050
051   private Derived(String from) {
052      Topics.data(from);
053      this.from = DataSourceDef.checkedId(from);
054   }
055
056   /**
057    * @param source The input source id.
058    * @return A new declaration.
059    * @throws IllegalArgumentException E-40 if the id is not a valid topic key.
060    */
061   public static Derived from(String source) {
062      return new Derived(source);
063   }
064
065   /**
066    * Appends a transform.
067    *
068    * @param name A registered transform (bare built-in or dotted app name).
069    * @param args The transform's arguments, or <jk>null</jk>.  The map is copied one level deep; nested values are
070    *    shared, so do not mutate them after the call.
071    * @return This object.
072    */
073   public Derived transform(String name, JsonMap args) {
074      DataSourceDef.extensionName(name, "transform");
075      var t = JsonMap.of("use", name);
076      t.put("args", args == null ? new JsonMap() : new JsonMap(args));
077      transforms.add(t);
078      return this;
079   }
080
081   /** @param subs Subscriptions with role <c>params</c>. @return This object. */
082   public Derived params(Subscription...subs) {
083      for (var s : subs) {
084         if (s == null)
085            throw iaex(DataSourceDef.E76, id != null ? id : from, "subscription is null");
086         params.add(s);
087      }
088      return this;
089   }
090
091   /**
092    * Names the output.
093    *
094    * @param value The derived source id; the topic is <c>data:&lt;value&gt;</c>.
095    * @return This object.
096    */
097   public Derived as(String value) {
098      Topics.data(value);
099      if (value.equals(from))
100         throw iaex(DataSourceDef.E76, value, "a derived source cannot read its own topic");
101      id = DataSourceDef.checkedId(value);
102      return this;
103   }
104
105   /** @return The output id, or <jk>null</jk> before {@link #as(String)}. */
106   public String id() { return id; }
107
108   /**
109    * The <c>contract.dataSources[]</c> entry.
110    *
111    * @return A new map.
112    * @throws IllegalStateException If {@link #as(String)} was not called.
113    */
114   public JsonMap toMap() {
115      if (id == null)
116         throw isex("Derived.from('%s'): as(id) not called", from);
117      var subs = new ArrayList<Object>();
118      for (var s : params) {
119         var m = s.toMap();
120         if (! "params".equals(m.get("as")))
121            throw iaex(DataSourceDef.E76, id,
122               f("subscription to '%s' has role '%s'; a derived source accepts params", s.topic(), m.get("as")));
123         subs.add(m);
124      }
125      var copies = transforms.stream().map(t -> {
126         var c = new JsonMap(t);
127         c.put("args", new JsonMap((JsonMap)t.get("args")));
128         return c;
129      }).toList();
130      return JsonMap.of("id", id, "kind", "derived", "from", from, "transforms", copies, "subscribes", subs);
131   }
132}