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 — 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}