001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 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    /** Creates a new instance of Messages */
134    public Messages() {
135        super();
136    }
137    
138    /**
139     * Create an <code>Messages</code> object initialized with the given
140     * messages.
141     *
142     * @param messages The messages to be initially added to this object.
143     * This parameter can be <code>null</code>.
144     * @since Struts 1.1
145     */
146    /** Creates a new instance of Messages */
147    public Messages(Messages messages) {
148        super();
149        this.add(messages);
150    }
151    
152    /**
153     * Add a message to the set of messages for the specified property.  An
154     * order of the property/key is maintained based on the initial addition
155     * of the property/key.
156     *
157     * @param property  Property name (or Messages.GLOBAL_MESSAGE)
158     * @param message   The message to be added
159     */
160    public Messages add(String property, Message message) {
161        MessageItem item = messages.get(property);
162        Map<String, Message> hashMap = null;
163        
164        if(item == null) {
165            hashMap = new HashMap<>();
166            item = new MessageItem(hashMap, iCount++);
167            
168            messages.put(property, item);
169        } else {
170            hashMap = item.getHashMap();
171        }
172        
173        hashMap.put(message.getKey(), message);
174
175        return this;
176    }
177    
178    /**
179     * Adds the messages from the given <code>Messages</code> object to
180     * this set of messages.  The messages are added in the order they are returned from
181     * the properties() method.  If a message's property is already in the current
182     * <code>Messages</code> object it is added to the end of the list for that
183     * property.  If a message's property is not in the current list it is added to the end
184     * of the properties.
185     *
186     * @param messages The <code>Messages</code> object to be added.
187     * This parameter can be <code>null</code>.
188     * @since Struts 1.1
189     */
190    public Messages add(Messages messages) {
191        if(messages == null) {
192            return this;
193        }
194        // loop over properties
195        Iterator props = messages.properties();
196        while(props.hasNext()) {
197            String property = (String) props.next();
198            
199            // loop over messages for each property
200            Iterator msgs = messages.get(property);
201            while(msgs.hasNext()) {
202                Message msg = (Message)msgs.next();
203                this.add(property, msg);
204            }
205        }
206
207        return this;
208    }
209    
210    /**
211     * Clear all messages recorded by this object.
212     */
213    public void clear() {
214        messages.clear();
215    }
216    
217    /**
218     * Return <code>true</code> if there are no messages recorded
219     * in this collection, or <code>false</code> otherwise.
220     * @since Struts 1.1
221     */
222    public boolean isEmpty(){
223        return messages.isEmpty();
224    }
225    
226    /**
227     * Return the set of all recorded messages, without distinction
228     * by which property the messages are associated with.  If there are
229     * no messages recorded, an empty enumeration is returned.
230     */
231    public Iterator<Message> get() {
232        if(messages.isEmpty()) {
233            return Collections.<Message>emptyList().iterator();
234        }
235        
236        List<Message> results = new ArrayList<>();
237        List<MessageItem> actionItems = new ArrayList<>();
238        
239        for(Iterator<MessageItem> i = messages.values().iterator(); i.hasNext();) {
240            actionItems.add(i.next());
241        }
242        
243        // Sort MessageItems based on the initial order the
244        // property/key was added to Messages.
245        Collections.sort(actionItems, (MessageItem o1, MessageItem o2) -> o1.getOrder() - o2.getOrder());
246        
247        actionItems.forEach((ami) -> {
248            for(Iterator<Message> messages = ami.getHashMap().values().iterator(); messages.hasNext();) {
249                results.add(messages.next());
250            }
251        });
252        
253        return results.iterator();
254    }
255    
256    /**
257     * Return the set of messages related to a specific property.
258     * If there are no such messages, an empty enumeration is returned.
259     *
260     * @param property Property name (or Messages.GLOBAL_MESSAGE)
261     */
262    public Iterator<Message> get(String property) {
263        MessageItem item = (MessageItem) messages.get(property);
264        
265        if(item == null) {
266            return Collections.<Message>emptyList().iterator();
267        } else {
268            return item.getHashMap().values().iterator();
269        }
270    }
271    
272    public boolean containsKey(String property, String key) {
273        MessageItem item = (MessageItem)messages.get(property);
274        boolean result;
275        
276        if(item != null) {
277            result = item.getHashMap().containsKey(key);
278        } else {
279            result = false;
280        }
281        
282        return result;
283    }
284    
285    public boolean containsKeys(String property, String... keys) {
286        MessageItem item = (MessageItem)messages.get(property);
287        boolean result = false;
288        
289        if(item != null) {
290            for(String key : Arrays.asList(keys)) {
291                result = item.getHashMap().containsKey(key);
292                if(result) {
293                    break;
294                }
295            }
296        }
297        
298        return result;
299    }
300    
301    /**
302     * Return the set of property names for which at least one message has
303     * been recorded.  If there are no messages, an empty Iterator is returned.
304     * If you have recorded global messages, the String value of
305     * <code>Messages.GLOBAL_MESSAGE</code> will be one of the returned
306     * property names.
307     */
308    public Iterator<String> properties() {
309        return messages.keySet().iterator();
310    }
311    
312    /**
313     * Return the number of messages recorded for all properties (including
314     * global messages).  <strong>NOTE</strong> - it is more efficient to call
315     * <code>empty()</code> if all you care about is whether or not there are
316     * any messages at all.
317     */
318    public int size() {
319        int total = 0;
320        
321        for(Iterator i = messages.values().iterator(); i.hasNext();) {
322            MessageItem ami = (MessageItem)i.next();
323            total += ami.getHashMap().size();
324        }
325        
326        return total;
327    }
328    
329    /**
330     * Return the number of messages associated with the specified property.
331     *
332     * @param property Property name (or Messages.GLOBAL_MESSAGE)
333     */
334    public int size(String property) {
335        MessageItem ami = (MessageItem) messages.get(property);
336        
337        if(ami == null) {
338            return 0;
339        } else {
340            return ami.getHashMap().size();
341        }
342    }
343    
344    /**
345     * This class is used to store a set of messages associated with a
346     * property/key and the position it was initially added to list.
347     */
348    protected static class MessageItem
349            implements Serializable {
350        
351        /**
352         * The list of <code>Message</code>s.
353         */
354        protected Map<String, Message> hashMap = null;
355        
356        /**
357         * The position in the list of messages.
358         */
359        protected int iOrder = 0;
360        
361        public MessageItem(Map<String, Message> hashMap, int iOrder) {
362            this.hashMap = hashMap;
363            this.iOrder = iOrder;
364        }
365        
366        public Map<String, Message> getHashMap() {
367            return hashMap;
368        }
369        
370        public void setHashMap(Map<String, Message> hashMap) {
371            this.hashMap = hashMap;
372        }
373        
374        public int getOrder() {
375            return iOrder;
376        }
377        
378        public void setOrder(int iOrder) {
379            this.iOrder = iOrder;
380        }
381        
382        /**
383         * Converts to a string representing the data contained within this set of MessageItem.
384         */
385        @Override
386        public String toString() {
387            return new StringBuilder().append("{ hashMap = ").append(hashMap).append(", iOrder = ").append(iOrder).append(" }").toString();
388        }
389        
390    }
391    
392    /**
393     * Converts to a string representing the data contained within this set of Messages.
394     */
395    @Override
396    public String toString() {
397        return new StringBuilder().append("{ messages = ").append(messages).append(", iCount = ").append(iCount).append(" }").toString();
398    }
399    
400}