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.CollectionUtils.*;
020
021import java.util.*;
022import java.util.regex.*;
023
024/**
025 * Save and restore of a dashboard's state topics.
026 *
027 * <p>
028 * The listed topics are saved per browser under the namespace, 500 ms after the last change.  On load the first of
029 * these that yields a value wins: an app link decoder, then a {@code #s=} share link (with {@link #shareLink()}), then
030 * the saved state when its version matches, then each owner's default.  Bump {@link #version(int)} whenever a payload
031 * shape changes; saved state of another version is deleted.
032 * </p>
033 *
034 * <h5 class='section'>Example:</h5>
035 * <p class='bjava'>
036 *    PersistSpec.<jsm>page</jsm>(<js>"app.orders"</js>).version(1)
037 *       .topics(Topics.<jsm>filter</jsm>(<js>"range"</js>), Topics.<jsm>zoom</jsm>(<js>"daily"</js>))
038 *       .shareLink();
039 * </p>
040 *
041 * @since 10.0.0
042 */
043@SuppressWarnings({
044   "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention)
045})
046public final class PersistSpec {
047
048   private static final Pattern NS = Pattern.compile("^[a-z][a-z0-9.-]{0,63}$");
049   private static final Pattern STATE_TOPIC = Pattern.compile("^(filter|zoom|legend):[A-Za-z][A-Za-z0-9_-]{0,63}$");
050
051   private final String ns;
052   private int version = 1;
053   private final List<String> topics = new ArrayList<>();
054   private boolean shareLink;
055   private final List<String> decoders = new ArrayList<>();
056
057   private PersistSpec(String ns) {
058      this.ns = ns;
059   }
060
061   /**
062    * Creates a per-page persistence spec.
063    *
064    * @param ns The storage namespace, matching {@code ^[a-z][a-z0-9.-]{0,63}$}.
065    * @return A new spec.
066    */
067   public static PersistSpec page(String ns) {
068      return new PersistSpec(ns);
069   }
070
071   /**
072    * Sets the state version.
073    *
074    * @param value The version, at least 1.  Defaults to 1.
075    * @return This object.
076    */
077   public PersistSpec version(int value) {
078      version = value;
079      return this;
080   }
081
082   /**
083    * Appends the topics to save and restore.
084    *
085    * @param values {@code filter:}, {@code zoom:} or {@code legend:} topics owned by members of the dashboard.
086    * @return This object.
087    */
088   public PersistSpec topics(String...values) {
089      topics.addAll(List.of(values));
090      return this;
091   }
092
093   /**
094    * Enables {@code #s=} share links and the "Copy link" button.
095    *
096    * @return This object.
097    */
098   public PersistSpec shareLink() {
099      shareLink = true;
100      return this;
101   }
102
103   /**
104    * Appends an app link decoder, registered in JS with {@code JuneauDashboard.decoder.register(name, fn)}.
105    *
106    * <p>
107    * The decoder must be registered before {@code juneau-charts.js} loads (from the {@code window.JuneauConsoleDashboard}
108    * pre-load queue in a head script, or from a toolkit pack), because restore runs when the first chart mounts.  A page
109    * {@code init} script is too late.
110    * </p>
111    *
112    * @param name The dotted decoder name.
113    * @return This object.
114    */
115   public PersistSpec decoder(String name) {
116      decoders.add(name);
117      return this;
118   }
119
120   /** @return The topics, in order. */
121   List<String> topics() {
122      return topics;
123   }
124
125   /**
126    * Checks this spec, reporting E-77 under {@code dash}.
127    *
128    * @param dash The dashboard id.
129    */
130   void check(String dash) {
131      if (ns == null || ! NS.matcher(ns).matches())
132         throw Section.e77(dash, "namespace '" + ns + "' does not match " + NS.pattern());
133      if (version < 1)
134         throw Section.e77(dash, "persist version " + version + " must be at least 1");
135      if (topics.isEmpty())
136         throw Section.e77(dash, "persist needs at least one topic");
137      var seen = new HashSet<String>();
138      for (var t : topics) {
139         if (t == null || ! STATE_TOPIC.matcher(t).matches())
140            throw Section.e77(dash, "persist topic '" + t + "' is not a filter:, zoom: or legend: state topic");
141         if (! seen.add(t))
142            throw Section.e77(dash, "persist lists topic '" + t + "' twice");
143      }
144      var names = new HashSet<String>();
145      for (var d : decoders) {
146         if (! Section.isDottedName(d))
147            throw Section.e77(dash, "decoder name '" + d + "' is not a dotted extension name");
148         if (! names.add(d))
149            throw Section.e77(dash, "decoder '" + d + "' is listed twice");
150      }
151   }
152
153   /** @return The persist JSON. */
154   Map<String,Object> toMap() {
155      return map(
156         "ns", ns,
157         "version", version,
158         "topics", List.copyOf(topics),
159         "shareLink", shareLink,
160         "decoders", List.copyOf(decoders)
161      );
162   }
163}