001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017/* ==================================================================== 018 * 019 * The Apache Software License, Version 1.1 020 * 021 * Copyright (c) 1999-2003 The Apache Software Foundation. All rights 022 * reserved. 023 * 024 * Redistribution and use in source and binary forms, with or without 025 * modification, are permitted provided that the following conditions 026 * are met: 027 * 028 * 1. Redistributions of source code must retain the above copyright 029 * notice, this list of conditions and the following disclaimer. 030 * 031 * 2. Redistributions in binary form must reproduce the above copyright 032 * notice, this list of conditions and the following disclaimer in 033 * the documentation and/or other materials provided with the 034 * distribution. 035 * 036 * 3. The end-user documentation included with the redistribution, if 037 * any, must include the following acknowlegement: 038 * "This product includes software developed by the 039 * Apache Software Foundation (http://www.apache.org/)." 040 * Alternately, this acknowlegement may appear in the software itself, 041 * if and wherever such third-party acknowlegements normally appear. 042 * 043 * 4. The names "The Jakarta Project", "Struts", and "Apache Software 044 * Foundation" must not be used to endorse or promote products derived 045 * from this software without prior written permission. For written 046 * permission, please contact apache@apache.org. 047 * 048 * 5. Products derived from this software may not be called "Apache" 049 * nor may "Apache" appear in their names without prior written 050 * permission of the Apache Group. 051 * 052 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED 053 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 054 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 055 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR 056 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 057 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 058 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 059 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 060 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 061 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 062 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 063 * SUCH DAMAGE. 064 * ==================================================================== 065 * 066 * This software consists of voluntary contributions made by many 067 * individuals on behalf of the Apache Software Foundation. For more 068 * information on the Apache Software Foundation, please see 069 * <http://www.apache.org/>. 070 * 071 */ 072 073package com.echothree.util.common.message; 074 075import java.io.Serializable; 076import java.util.ArrayList; 077import java.util.Arrays; 078import java.util.Collections; 079import java.util.HashMap; 080import java.util.Iterator; 081import java.util.List; 082import java.util.Map; 083 084/** 085 * <p>A class that encapsulates messages. Messages can be either global 086 * or they are specific to a particular bean property.</p> 087 * 088 * <p>Each individual message is described by an <code>Message</code> 089 * object, which contains a message key (to be looked up in an appropriate 090 * message resources database), and up to four placeholder arguments used for 091 * parametric substitution in the resulting message.</p> 092 * 093 * <p><strong>IMPLEMENTATION NOTE</strong> - It is assumed that these objects 094 * are created and manipulated only within the context of a single thread. 095 * Therefore, no synchronization is required for access to internal 096 * collections.</p> 097 * 098 * @since Struts 1.1 099 */ 100 101public class Messages 102 implements Serializable { 103 104 // ----------------------------------------------------- Manifest Constants 105 106 /** 107 * The "property name" marker to use for BaseCommand messages, as opposed to 108 * those related to a specific property. 109 */ 110 public static final String EXECUTION_WARNING = "com.echothree.util.common.message.EXECUTION_WARNING"; 111 public static final String EXECUTION_ERROR = "com.echothree.util.common.message.EXECUTION_ERROR"; 112 public static final String SECURITY_MESSAGE = "com.echothree.util.common.message.SECURITY_MESSAGE"; 113 114 // ----------------------------------------------------- Instance Variables 115 116 /** 117 * The accumulated set of <code>Message</code> objects (represented 118 * as an ArrayList) for each property, keyed by property name. 119 */ 120 protected Map<String, MessageItem> messages = new HashMap<>(); 121 122 /** 123 * The current number of the property/key being added. This is used 124 * to maintain the order messages are added. 125 */ 126 protected int iCount = 0; 127 128 // --------------------------------------------------------- Public Methods 129 130 /** 131 * Create an empty <code>Messages</code> object. 132 */ 133 public Messages() { 134 super(); 135 } 136 137 /** 138 * Create an <code>Messages</code> object initialized with the given 139 * messages. 140 * 141 * @param messages The messages to be initially added to this object. 142 * This parameter can be <code>null</code>. 143 * @since Struts 1.1 144 */ 145 public Messages(Messages messages) { 146 super(); 147 this.add(messages); 148 } 149 150 /** 151 * Add a message to the set of messages for the specified property. An 152 * order of the property/key is maintained based on the initial addition 153 * of the property/key. 154 * 155 * @param property Property name (or Messages.GLOBAL_MESSAGE) 156 * @param message The message to be added 157 */ 158 public Messages add(String property, Message message) { 159 var item = messages.get(property); 160 Map<String, Message> hashMap; 161 162 if(item == null) { 163 hashMap = new HashMap<>(); 164 item = new MessageItem(hashMap, iCount++); 165 166 messages.put(property, item); 167 } else { 168 hashMap = item.getHashMap(); 169 } 170 171 hashMap.put(message.getKey(), message); 172 173 return this; 174 } 175 176 /** 177 * Adds the messages from the given <code>Messages</code> object to 178 * this set of messages. The messages are added in the order they are returned from 179 * the properties() method. If a message's property is already in the current 180 * <code>Messages</code> object it is added to the end of the list for that 181 * property. If a message's property is not in the current list it is added to the end 182 * of the properties. 183 * 184 * @param messages The <code>Messages</code> object to be added. 185 * This parameter can be <code>null</code>. 186 * @since Struts 1.1 187 */ 188 public Messages add(Messages messages) { 189 if(messages == null) { 190 return this; 191 } 192 // loop over properties 193 Iterator props = messages.properties(); 194 while(props.hasNext()) { 195 var property = (String) props.next(); 196 197 // loop over messages for each property 198 Iterator msgs = messages.get(property); 199 while(msgs.hasNext()) { 200 var msg = (Message)msgs.next(); 201 this.add(property, msg); 202 } 203 } 204 205 return this; 206 } 207 208 /** 209 * Clear all messages recorded by this object. 210 */ 211 public void clear() { 212 messages.clear(); 213 } 214 215 /** 216 * Return <code>true</code> if there are no messages recorded 217 * in this collection, or <code>false</code> otherwise. 218 * @since Struts 1.1 219 */ 220 public boolean isEmpty(){ 221 return messages.isEmpty(); 222 } 223 224 /** 225 * Return the set of all recorded messages, without distinction 226 * by which property the messages are associated with. If there are 227 * no messages recorded, an empty enumeration is returned. 228 */ 229 public Iterator<Message> get() { 230 if(messages.isEmpty()) { 231 return Collections.<Message>emptyList().iterator(); 232 } 233 234 List<Message> results = new ArrayList<>(); 235 List<MessageItem> actionItems = new ArrayList<>(); 236 237 for(var i = messages.values().iterator(); i.hasNext();) { 238 actionItems.add(i.next()); 239 } 240 241 // Sort MessageItems based on the initial order the 242 // property/key was added to Messages. 243 Collections.sort(actionItems, (MessageItem o1, MessageItem o2) -> o1.getOrder() - o2.getOrder()); 244 245 actionItems.forEach((ami) -> { 246 for(var messages = ami.getHashMap().values().iterator(); messages.hasNext();) { 247 results.add(messages.next()); 248 } 249 }); 250 251 return results.iterator(); 252 } 253 254 /** 255 * Return the set of messages related to a specific property. 256 * If there are no such messages, an empty enumeration is returned. 257 * 258 * @param property Property name (or Messages.GLOBAL_MESSAGE) 259 */ 260 public Iterator<Message> get(String property) { 261 var item = (MessageItem) messages.get(property); 262 263 if(item == null) { 264 return Collections.<Message>emptyList().iterator(); 265 } else { 266 return item.getHashMap().values().iterator(); 267 } 268 } 269 270 public boolean containsKey(String property, String key) { 271 var item = (MessageItem)messages.get(property); 272 boolean result; 273 274 if(item != null) { 275 result = item.getHashMap().containsKey(key); 276 } else { 277 result = false; 278 } 279 280 return result; 281 } 282 283 public boolean containsKeys(String property, String... keys) { 284 var item = (MessageItem)messages.get(property); 285 var result = false; 286 287 if(item != null) { 288 for(var key : Arrays.asList(keys)) { 289 result = item.getHashMap().containsKey(key); 290 if(result) { 291 break; 292 } 293 } 294 } 295 296 return result; 297 } 298 299 /** 300 * Return the set of property names for which at least one message has 301 * been recorded. If there are no messages, an empty Iterator is returned. 302 * If you have recorded global messages, the String value of 303 * <code>Messages.GLOBAL_MESSAGE</code> will be one of the returned 304 * property names. 305 */ 306 public Iterator<String> properties() { 307 return messages.keySet().iterator(); 308 } 309 310 /** 311 * Return the number of messages recorded for all properties (including 312 * global messages). <strong>NOTE</strong> - it is more efficient to call 313 * <code>empty()</code> if all you care about is whether or not there are 314 * any messages at all. 315 */ 316 public int size() { 317 var total = 0; 318 319 for(Iterator i = messages.values().iterator(); i.hasNext();) { 320 var ami = (MessageItem)i.next(); 321 total += ami.getHashMap().size(); 322 } 323 324 return total; 325 } 326 327 /** 328 * Return the number of messages associated with the specified property. 329 * 330 * @param property Property name (or Messages.GLOBAL_MESSAGE) 331 */ 332 public int size(String property) { 333 var ami = (MessageItem) messages.get(property); 334 335 if(ami == null) { 336 return 0; 337 } else { 338 return ami.getHashMap().size(); 339 } 340 } 341 342 /** 343 * This class is used to store a set of messages associated with a 344 * property/key and the position it was initially added to list. 345 */ 346 protected static class MessageItem 347 implements Serializable { 348 349 /** 350 * The list of <code>Message</code>s. 351 */ 352 protected Map<String, Message> hashMap; 353 354 /** 355 * The position in the list of messages. 356 */ 357 protected int iOrder; 358 359 public MessageItem(Map<String, Message> hashMap, int iOrder) { 360 this.hashMap = hashMap; 361 this.iOrder = iOrder; 362 } 363 364 public Map<String, Message> getHashMap() { 365 return hashMap; 366 } 367 368 public void setHashMap(Map<String, Message> hashMap) { 369 this.hashMap = hashMap; 370 } 371 372 public int getOrder() { 373 return iOrder; 374 } 375 376 public void setOrder(int iOrder) { 377 this.iOrder = iOrder; 378 } 379 380 /** 381 * Converts to a string representing the data contained within this set of MessageItem. 382 */ 383 @Override 384 public String toString() { 385 return "{ hashMap = " + hashMap + ", iOrder = " + iOrder + " }"; 386 } 387 388 } 389 390 /** 391 * Converts to a string representing the data contained within this set of Messages. 392 */ 393 @Override 394 public String toString() { 395 return "{ messages = " + messages + ", iCount = " + iCount + " }"; 396 } 397 398}