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.widgets;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.regex.*;
022
023import org.apache.juneau.commons.bean.*;
024import org.apache.juneau.commons.http.*;
025
026/**
027 * A single row in an attached header or avatar menu.
028 *
029 * <p>
030 * A menu item is exactly one of a <b>link</b> (same-origin navigation), a <b>safe</b> (format-validated client-event
031 * token, host-interpreted), or a <b>divider</b> (a separator row).  Dividers are <b>exempt</b> from the id/label/href/
032 * safe rules entirely: a fail-closed validate must not reject a real menu just because it contains a separator, and
033 * {@link #divider()} mints no id.
034 *
035 * @since 10.0.0
036 */
037@BeanType(properties="id,label,icon,href,safe,divider")
038@SuppressWarnings({
039   "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
040})
041public class MenuItem {
042
043   /** The client-safe token charset: lowercase letter, then lowercase / digit / hyphen, max 64 chars. */
044   private static final Pattern SAFE_TOKEN = Pattern.compile("[a-z][a-z0-9-]{0,63}");
045
046   /** Required + unique for link/safe items; <b>ignored</b> for dividers. */
047   public String id;
048
049   /** The row text, painted as {@code textContent}; ignored for dividers. */
050   public String label;
051
052   /** Optional icon-registry name, hydrated client-side from {@code data-juneau-icon}. */
053   public String icon;
054
055   /** A same-origin navigation target (link item); ignored for dividers. */
056   public String href;
057
058   /** A format-validated client-event token (safe item); ignored for dividers. */
059   public String safe;
060
061   /** When {@link Boolean#TRUE}, this is a separator row and all other fields are ignored. */
062   public Boolean divider;
063
064   /**
065    * Creates a navigation (link) menu item.
066    *
067    * @param id The stable item id.  Must not be <jk>null</jk> or blank.
068    * @param label The row text.  Must not be <jk>null</jk> or blank.
069    * @param href The same-origin navigation target.  Must not be <jk>null</jk> or blank.
070    * @return A new {@link MenuItem}.
071    */
072   public static MenuItem link(String id, String label, String href) {
073      var m = new MenuItem();
074      m.id = id;
075      m.label = label;
076      m.href = href;
077      return m;
078   }
079
080   /**
081    * Creates a client-safe menu item.
082    *
083    * @param id The stable item id.  Must not be <jk>null</jk> or blank.
084    * @param label The row text.  Must not be <jk>null</jk> or blank.
085    * @param safeToken The client-event token; must match {@code ^[a-z][a-z0-9-]{0,63}$}.
086    * @return A new {@link MenuItem}.
087    */
088   public static MenuItem safe(String id, String label, String safeToken) {
089      var m = new MenuItem();
090      m.id = id;
091      m.label = label;
092      m.safe = safeToken;
093      return m;
094   }
095
096   /**
097    * Creates a divider (separator) row.  Needs no id &mdash; it is exempt from the unique-id rule.
098    *
099    * @return A new divider {@link MenuItem}.
100    */
101   public static MenuItem divider() {
102      var m = new MenuItem();
103      m.divider = true;
104      return m;
105   }
106
107   /**
108    * Sets the optional icon-registry name.
109    *
110    * @param value The icon name.
111    * @return This object.
112    */
113   public MenuItem icon(String value) {
114      icon = value;
115      return this;
116   }
117
118   /**
119    * Whether this item is a divider (separator) row.
120    *
121    * @return <jk>true</jk> if this is a divider.
122    */
123   public boolean isDivider() {
124      return divider != null && divider;
125   }
126
127   /**
128    * Fail-closed bean validation; called by the enclosing menu host.
129    *
130    * <p>
131    * Dividers are exempt: a divider validates trivially regardless of its other (ignored) fields.
132    *
133    * @throws IllegalArgumentException If this menu item is not well-formed.
134    */
135   public void validate() {
136      if (isDivider())
137         return;
138      if (ib(id))
139         throw iaex("MenuItem id must not be null or blank.");
140      if (ib(label))
141         throw iaex("MenuItem '%s' label must not be null or blank.", id);
142      var hasHref = inb(href);
143      var hasSafe = inb(safe);
144      if (hasHref == hasSafe)
145         throw iaex("MenuItem '%s' must declare exactly one of link/safe.", id);
146      if (hasHref && !SafePathTemplate.isSameOriginPath(href))
147         throw iaex("MenuItem '%s' href must be a same-origin path (no absolute URL, '//', scheme, '..', "
148            + "'data:', or 'javascript:'): %s", id, href);
149      if (hasSafe && !SAFE_TOKEN.matcher(safe).matches())
150         throw iaex("MenuItem '%s' safe token must match ^[a-z][a-z0-9-]{0,63}$: %s", id, safe);
151   }
152}