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