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}