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;
018
019import java.util.*;
020import java.util.regex.*;
021
022/**
023 * A reference to a client-side handler: a registered name or an opt-in inline body.
024 *
025 * <p>
026 * Names use the {@code ns.name} grammar and are registered on the page with {@code JuneauViews.handlers.register}.
027 * The {@code juneau.*} namespace is reserved for the built-ins (see {@link #builtin(String)}).
028 *
029 * <h2 class='section'>Example:</h2>
030 * <p class='bjava'>
031 *   HandlerRef <jv>a</jv> = HandlerRef.<jsm>named</jsm>(<js>"orders.refresh"</js>);
032 *   HandlerRef <jv>b</jv> = HandlerRef.<jsm>builtin</jsm>(<js>"refresh"</js>);   <jc>// juneau.refresh</jc>
033 * </p>
034 *
035 * @since 10.0.0
036 */
037@SuppressWarnings({
038   "java:S1845", // Instance field name and the NAME pattern constant are distinct, conventionally named members.
039   "java:S5998" // Handler names are short, so the regex repetition depth is bounded.
040})
041public final class HandlerRef {
042
043   private static final Pattern NAME = Pattern.compile("^[a-z][A-Za-z0-9]*(\\.[a-z][A-Za-z0-9-]*)+$");
044
045   /** The built-in handler names, without the {@code juneau.} prefix. */
046   static final Set<String> BUILTINS = Set.of("refresh", "refreshClosest", "expand", "collapse", "activate", "publish", "countLabel", "countBadge");
047
048   private final String name;      // null when inline
049   private final String inline;    // null when named
050
051   private HandlerRef(String name, String inline) {
052      this.name = name;
053      this.inline = inline;
054   }
055
056   /**
057    * A registered handler name.
058    *
059    * @param name The name, e.g. {@code orders.refresh}.  A {@code juneau.}-prefixed name is accepted only when it names a built-in.
060    * @return A new reference.
061    * @throws IllegalArgumentException If the name is malformed or uses the reserved namespace.
062    */
063   public static HandlerRef named(String name) {
064      if (name == null || ! NAME.matcher(name).matches())
065         throw new IllegalArgumentException("Handler name '" + name + "' must match ns.name (lower-case first letter per segment, at least one dot).");
066      if (name.startsWith("juneau.") && ! BUILTINS.contains(name.substring(7)))
067         throw new IllegalArgumentException("Handler name '" + name + "' uses the reserved juneau.* namespace.");
068      return new HandlerRef(name, null);
069   }
070
071   /**
072    * A built-in handler.
073    *
074    * @param name The built-in name without prefix, e.g. {@code refresh}.
075    * @return A reference to {@code juneau.<name>}.
076    * @throws IllegalArgumentException If the name is not a built-in.
077    */
078   public static HandlerRef builtin(String name) {
079      if (! BUILTINS.contains(name))
080         throw new IllegalArgumentException("'" + name + "' is not a built-in handler: " + new TreeSet<>(BUILTINS));
081      return new HandlerRef("juneau." + name, null);
082   }
083
084   /**
085    * An inline handler body, compiled on the client as {@code function(target, event)}.
086    * Needs {@code PageSpec.allowInlineHandlers(true)}.
087    *
088    * @param body The function body.
089    * @return A new reference.
090    * @throws IllegalArgumentException If the body is blank.
091    */
092   public static HandlerRef inline(String body) {
093      if (body == null || body.isBlank())
094         throw new IllegalArgumentException("Inline handler body must not be empty.");
095      return new HandlerRef(null, body);
096   }
097
098   /**
099    * Returns the handler name.
100    *
101    * @return The name, or <jk>null</jk> for an inline reference.
102    */
103   public String name() {
104      return name;
105   }
106
107   /**
108    * Returns whether this is an inline body.
109    *
110    * @return <jk>true</jk> if inline.
111    */
112   public boolean isInline() {
113      return inline != null;
114   }
115
116   /**
117    * Returns the contract JSON form.
118    *
119    * @return The name string, or {@code {"inline": body}}.
120    */
121   public Object toJson() {
122      return inline != null ? Map.of("inline", inline) : name;
123   }
124
125   @Override /* Object */
126   public boolean equals(Object o) {
127      return o instanceof HandlerRef r && Objects.equals(name, r.name) && Objects.equals(inline, r.inline);
128   }
129
130   @Override /* Object */
131   public int hashCode() {
132      return Objects.hash(name, inline);
133   }
134
135   @Override /* Object */
136   public String toString() {
137      return inline != null ? "inline:" + inline : name;
138   }
139}