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.wicket.feedback;
018
019import java.util.List;
020
021import org.apache.wicket.Component;
022import org.apache.wicket.MetaDataKey;
023import org.apache.wicket.Session;
024import org.apache.wicket.markup.html.panel.FeedbackPanel;
025
026/**
027 * A specialized feedback panel that only displays messages from inside a fence defined by a
028 * container component. Instances will not show messages coming from inside a nested fence, allowing
029 * the nesting of these panels to work correctly without displaying the same feedback message twice.
030 * A constructor that does not takes a fencing component creates a catch-all panel that shows
031 * messages that do not come from inside any fence or from the {@link Session}.
032 * <p/>
033 * <h2>IN DEPTH EXPLANATION</h2>
034 * <p>
035 * It is often very useful to have feedback panels that show feedback that comes from inside a
036 * certain container only. For example given a page with the following structure:
037 * </p>
038 * <p/>
039 * <pre>
040 * Page
041 *   Form1
042 *     Feedback1
043 *     Input1
044 *     Form2
045 *       Feedback2
046 *       Input2
047 * </pre>
048 * <p>
049 * we want Feedback2 to show messages originating only from inside Form2 and Feedback1 to show
050 * messages only originating from Form1 but not Form2 (because messages originating from Form2 are
051 * already shown by Feedback2).
052 * </p>
053 * <p>
054 * It is fairly simple to configure Feedback2 - a {@link ContainerFeedbackMessageFilter} added to
055 * the regular {@link FeedbackPanel} will do the trick. The hard part is configuring Feedback1. We
056 * can add a {@link ContainerFeedbackMessageFilter} to it, but since Form2 is inside Form1 the
057 * container filter will allow messages from both Form1 and Form2 to be added to FeedbackPanel1.
058 * </p>
059 * <p>
060 * This is where the {@link FencedFeedbackPanel} comes in. All we have to do is to make
061 * FeedbackPanel2 a {@link FencedFeedbackPanel} with the fencing component defined as Form2 and
062 * Feedback1 a {@link FencedFeedbackPanel} with the fencing component defined as Form1.
063 * {@link FencedFeedbackPanel} will only show messages that original from inside its fencing
064 * component and not from inside any descendant component that acts as a fence for another
065 * {@link FencedFeedbackPanel}.
066 * </p>
067 * <p>
068 * When created with a {@code null} fencing component or using a constructor that does not take one
069 * the panel will only display messages that do not come from inside a fence. It will also display
070 * messages that come from {@link Session}. This acts as a catch-all panels showing messages that
071 * would not be shown using any other instance of the {@link FencedFeedbackPanel} created with a
072 * fencing component. There is usually one instance of such a panel at the top of the page to
073 * display notifications of success.
074 * </p>
075 *
076 * @author igor
077 */
078public class FencedFeedbackPanel extends FeedbackPanel
079{
080        
081        private static final long serialVersionUID = 1L;
082
083        private static final MetaDataKey<Integer> FENCE_KEY = new MetaDataKey<>()
084        {
085                private static final long serialVersionUID = 1L;
086        };
087
088        private final Component fence;
089
090        /**
091         * Creates a catch-all feedback panel that will show messages not coming from any fence,
092         * including messages coming from {@link Session}
093         *
094         * @param id
095         */
096        public FencedFeedbackPanel(String id)
097        {
098                this(id, (Component)null);
099        }
100
101        /**
102         * Creates a feedback panel that will only show messages if they original from, or inside of,
103         * the {@code fence} component and not from any inner fence.
104         *
105         * @param id
106         * @param fence
107         */
108        public FencedFeedbackPanel(String id, Component fence)
109        {
110                this(id, fence, null);
111        }
112
113        /**
114         * Creates a catch-all instance with a filter.
115         *
116         * @param id
117         * @param filter
118         * @see #FencedFeedbackPanel(String)
119         */
120        public FencedFeedbackPanel(String id, IFeedbackMessageFilter filter)
121        {
122                this(id, null, filter);
123        }
124
125        /**
126         * Creates a fenced feedback panel with a filter.
127         *
128         * @param id
129         * @param fence
130         * @param filter
131         * @see #FencedFeedbackPanel(String, Component)
132         */
133        public FencedFeedbackPanel(String id, Component fence, IFeedbackMessageFilter filter)
134        {
135                super(id, filter);
136                this.fence = fence;
137                if (fence != null)
138                {
139                        incrementFenceCount();
140                }
141        }
142
143        private void incrementFenceCount()
144        {
145                Integer count = fence.getMetaData(FENCE_KEY);
146                count = count == null ? 1 : count + 1;
147                fence.setMetaData(FENCE_KEY, count);
148        }
149
150        @Override
151        protected void onRemove()
152        {
153                super.onRemove();
154                if (fence != null)
155                {
156                        // decrement the fence count
157
158                        decrementFenceCount();
159                }
160        }
161
162        private void decrementFenceCount()
163        {
164                Integer count = fence.getMetaData(FENCE_KEY);
165                count = (count == null || count == 1) ? null : count - 1;
166                fence.setMetaData(FENCE_KEY, count);
167        }
168
169        @Override
170        protected FeedbackMessagesModel newFeedbackMessagesModel()
171        {
172                return new FeedbackMessagesModel(this)
173                {
174                        private static final long serialVersionUID = 1L;
175
176                        @Override
177                        protected List<FeedbackMessage> collectMessages(Component panel,
178                                        IFeedbackMessageFilter filter)
179                        {
180                                if (fence == null)
181                                {
182                                        // this is the catch-all panel
183                                        return new FencedFeedbackCollector(panel.getPage(), true).collect(filter);
184                                }
185                                else
186                                {
187                                        // this is a fenced panel
188                                        return new FencedFeedbackCollector(fence, false).collect(filter);
189                                }
190                        }
191                };
192        }
193
194        @Override
195        protected void onReAdd()
196        {
197                if (this.fence != null)
198                {
199                        // The fence mark is removed when the feedback panel is removed from the hierarchy.
200                        // see onRemove().
201                        // when the panel is re-added, we recreate the fence mark.
202                        incrementFenceCount();
203                }
204                super.onReAdd();
205        }
206        
207        private final class FencedFeedbackCollector extends FeedbackCollector
208        {
209                private FencedFeedbackCollector(Component component, boolean includeSession)
210                {
211                        super(component, includeSession);
212                }
213
214                @Override
215                protected boolean shouldRecurseInto(Component component)
216                {
217                        return !componentIsMarkedAsFence(component);
218                }
219                
220                private boolean componentIsMarkedAsFence(Component component)
221                {
222                        return component.getMetaData(FENCE_KEY) != null;
223                }
224        }
225}