001    // Copyright 2006, 2007, 2008, 2009, 2010, 2011, 2012 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    // http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.internal.structure;
016    
017    import org.apache.tapestry5.ComponentResources;
018    import org.apache.tapestry5.internal.services.PersistentFieldManager;
019    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
020    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
021    import org.apache.tapestry5.ioc.internal.util.OneShotLock;
022    import org.apache.tapestry5.ioc.services.PerThreadValue;
023    import org.apache.tapestry5.ioc.services.PerthreadManager;
024    import org.apache.tapestry5.runtime.Component;
025    import org.apache.tapestry5.runtime.PageLifecycleListener;
026    import org.apache.tapestry5.services.PersistentFieldBundle;
027    import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
028    import org.slf4j.Logger;
029    
030    import java.util.List;
031    import java.util.Map;
032    import java.util.concurrent.atomic.AtomicInteger;
033    import java.util.regex.Pattern;
034    
035    public class PageImpl implements Page
036    {
037        private final String name;
038    
039        private final ComponentResourceSelector selector;
040    
041        private final PersistentFieldManager persistentFieldManager;
042    
043        private ComponentPageElement rootElement;
044    
045        private List<Runnable> loadedCallbacks = CollectionFactory.newList();
046    
047        private final List<Runnable> attachCallbacks = CollectionFactory.newList();
048    
049        private final List<Runnable> detachCallbacks = CollectionFactory.newList();
050    
051        private final List<Runnable> resetCallbacks = CollectionFactory.newList();
052    
053        private boolean loadComplete;
054    
055        private final OneShotLock lifecycleListenersLock = new OneShotLock();
056    
057        private final OneShotLock verifyListenerLocks = new OneShotLock();
058    
059        // May end up with multiple mappings for the same id (with different case) to the same component.
060        // That's OK.
061        private final Map<String, ComponentPageElement> idToComponent = CollectionFactory.newConcurrentMap();
062    
063        private Stats stats;
064    
065        private final AtomicInteger attachCount = new AtomicInteger();
066    
067        private List<Runnable> pageVerifyCallbacks = CollectionFactory.newList();
068    
069        /**
070         * Obtained from the {@link org.apache.tapestry5.internal.services.PersistentFieldManager} when
071         * first needed,
072         * discarded at the end of the request.
073         */
074        private final PerThreadValue<PersistentFieldBundle> fieldBundle;
075    
076        private static final Pattern SPLIT_ON_DOT = Pattern.compile("\\.");
077    
078        /**
079         * @param name
080         *         canonicalized page name
081         * @param selector
082         *         used to locate resources
083         * @param persistentFieldManager
084         *         for access to cross-request persistent values
085         * @param perThreadManager
086         *         for managing per-request mutable state
087         */
088        public PageImpl(String name, ComponentResourceSelector selector, PersistentFieldManager persistentFieldManager,
089                        PerthreadManager perThreadManager)
090        {
091            this.name = name;
092            this.selector = selector;
093            this.persistentFieldManager = persistentFieldManager;
094    
095            fieldBundle = perThreadManager.createValue();
096        }
097    
098        public void setStats(Stats stats)
099        {
100            this.stats = stats;
101        }
102    
103        public Stats getStats()
104        {
105            return stats;
106        }
107    
108        @Override
109        public String toString()
110        {
111            return String.format("Page[%s %s]", name, selector.toShortString());
112        }
113    
114        public ComponentPageElement getComponentElementByNestedId(String nestedId)
115        {
116            assert nestedId != null;
117    
118            if (nestedId.equals(""))
119            {
120                return rootElement;
121            }
122    
123            ComponentPageElement element = idToComponent.get(nestedId);
124    
125            if (element == null)
126            {
127                element = rootElement;
128    
129                for (String id : SPLIT_ON_DOT.split(nestedId))
130                {
131                    element = element.getEmbeddedElement(id);
132                }
133    
134                idToComponent.put(nestedId, element);
135            }
136    
137            return element;
138        }
139    
140        public ComponentResourceSelector getSelector()
141        {
142            return selector;
143        }
144    
145        public void setRootElement(ComponentPageElement component)
146        {
147            lifecycleListenersLock.check();
148    
149            rootElement = component;
150        }
151    
152        public ComponentPageElement getRootElement()
153        {
154            return rootElement;
155        }
156    
157        public Component getRootComponent()
158        {
159            return rootElement.getComponent();
160        }
161    
162        public void addLifecycleListener(final PageLifecycleListener listener)
163        {
164            assert listener != null;
165    
166            addPageLoadedCallback(new Runnable()
167            {
168                @Override
169                public void run()
170                {
171                    listener.containingPageDidLoad();
172                }
173            });
174    
175            addPageAttachedCallback(new Runnable()
176            {
177                @Override
178                public void run()
179                {
180                    listener.containingPageDidAttach();
181                }
182            });
183    
184            addPageDetachedCallback(new Runnable()
185            {
186                @Override
187                public void run()
188                {
189                    listener.containingPageDidDetach();
190                }
191            });
192        }
193    
194        public void removeLifecycleListener(PageLifecycleListener listener)
195        {
196            lifecycleListenersLock.check();
197    
198            throw new UnsupportedOperationException("It is not longer possible to remove a page lifecycle listener; please convert your code to use the addPageLoadedCallback() method instead.");
199        }
200    
201        public boolean detached()
202        {
203            boolean result = false;
204    
205            for (Runnable callback : detachCallbacks)
206            {
207                try
208                {
209                    callback.run();
210                } catch (RuntimeException ex)
211                {
212                    result = true;
213    
214                    getLogger().error(String.format("Callback %s failed during page detach: %s", callback, InternalUtils.toMessage(ex)), ex);
215                }
216            }
217    
218            return result;
219        }
220    
221        public void loaded()
222        {
223            lifecycleListenersLock.lock();
224    
225            invokeCallbacks(loadedCallbacks);
226    
227            loadedCallbacks = null;
228    
229            verifyListenerLocks.lock();
230    
231            invokeCallbacks(pageVerifyCallbacks);
232    
233            pageVerifyCallbacks = null;
234    
235            loadComplete = true;
236        }
237    
238        public void attached()
239        {
240            attachCount.incrementAndGet();
241    
242            invokeCallbacks(attachCallbacks);
243        }
244    
245        public Logger getLogger()
246        {
247            return rootElement.getLogger();
248        }
249    
250        public void persistFieldChange(ComponentResources resources, String fieldName, Object newValue)
251        {
252            if (!loadComplete)
253            {
254                throw new RuntimeException(StructureMessages.persistChangeBeforeLoadComplete());
255            }
256    
257            persistentFieldManager.postChange(name, resources, fieldName, newValue);
258        }
259    
260        public Object getFieldChange(String nestedId, String fieldName)
261        {
262            if (!fieldBundle.exists())
263            {
264                fieldBundle.set(persistentFieldManager.gatherChanges(name));
265            }
266    
267            return fieldBundle.get().getValue(nestedId, fieldName);
268        }
269    
270        public void discardPersistentFieldChanges()
271        {
272            persistentFieldManager.discardChanges(name);
273        }
274    
275        public String getName()
276        {
277            return name;
278        }
279    
280        @Override
281        public void addResetCallback(Runnable callback)
282        {
283            assert callback != null;
284    
285            lifecycleListenersLock.check();
286    
287            resetCallbacks.add(callback);
288        }
289    
290        public void addResetListener(final PageResetListener listener)
291        {
292            assert listener != null;
293    
294            addResetCallback(new Runnable()
295            {
296                @Override
297                public void run()
298                {
299                    listener.containingPageDidReset();
300                }
301            });
302        }
303    
304        public void addVerifyCallback(Runnable callback)
305        {
306            verifyListenerLocks.check();
307    
308            assert callback != null;
309    
310            pageVerifyCallbacks.add(callback);
311        }
312    
313        public void pageReset()
314        {
315            invokeCallbacks(resetCallbacks);
316        }
317    
318        public boolean hasResetListeners()
319        {
320            return !resetCallbacks.isEmpty();
321        }
322    
323        public int getAttachCount()
324        {
325            return attachCount.get();
326        }
327    
328        @Override
329        public void addPageLoadedCallback(Runnable callback)
330        {
331            lifecycleListenersLock.check();
332    
333            assert callback != null;
334    
335            loadedCallbacks.add(callback);
336        }
337    
338        @Override
339        public void addPageAttachedCallback(Runnable callback)
340        {
341            lifecycleListenersLock.check();
342    
343            assert callback != null;
344    
345            attachCallbacks.add(callback);
346        }
347    
348        @Override
349        public void addPageDetachedCallback(Runnable callback)
350        {
351            lifecycleListenersLock.check();
352    
353            assert callback != null;
354    
355            detachCallbacks.add(callback);
356        }
357    
358        private void invokeCallbacks(List<Runnable> callbacks)
359        {
360            for (Runnable callback : callbacks)
361            {
362                callback.run();
363            }
364        }
365    }