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}