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.commons.ObjectCreator;
020import org.apache.tapestry5.commons.util.CollectionFactory;
021import org.apache.tapestry5.commons.util.ExceptionUtils;
022import org.apache.tapestry5.internal.services.PersistentFieldManager;
023import org.apache.tapestry5.ioc.internal.util.OneShotLock;
024import org.apache.tapestry5.ioc.services.PerthreadManager;
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 ObjectCreator<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(new ObjectCreator<PersistentFieldBundle>() {
100            @Override
101            public PersistentFieldBundle createObject() {
102                return PageImpl.this.persistentFieldManager.gatherChanges(PageImpl.this.name);
103            }
104        });
105
106
107        exactParameterCountMatch = metaDataLocator.findMeta(MetaDataConstants.UNKNOWN_ACTIVATION_CONTEXT_CHECK, name, Boolean.class);
108    }
109
110    public void setStats(Stats stats)
111    {
112        this.stats = stats;
113    }
114
115    public Stats getStats()
116    {
117        return stats;
118    }
119
120    @Override
121    public String toString()
122    {
123        return String.format("Page[%s %s]", name, selector.toShortString());
124    }
125
126    public ComponentPageElement getComponentElementByNestedId(String nestedId)
127    {
128        assert nestedId != null;
129
130        if (nestedId.equals(""))
131        {
132            return rootElement;
133        }
134
135        ComponentPageElement element = idToComponent.get(nestedId);
136
137        if (element == null)
138        {
139            element = rootElement;
140
141            for (String id : SPLIT_ON_DOT.split(nestedId))
142            {
143                element = element.getEmbeddedElement(id);
144            }
145
146            idToComponent.put(nestedId, element);
147        }
148
149        return element;
150    }
151
152    public ComponentResourceSelector getSelector()
153    {
154        return selector;
155    }
156
157    public void setRootElement(ComponentPageElement component)
158    {
159        lifecycleListenersLock.check();
160
161        rootElement = component;
162    }
163
164    public ComponentPageElement getRootElement()
165    {
166        return rootElement;
167    }
168
169    public Component getRootComponent()
170    {
171        return rootElement.getComponent();
172    }
173
174    public void addLifecycleListener(final PageLifecycleListener listener)
175    {
176        assert listener != null;
177
178        addPageLoadedCallback(new Runnable()
179        {
180            public void run()
181            {
182                listener.containingPageDidLoad();
183            }
184        });
185
186        addPageAttachedCallback(new Runnable()
187        {
188            public void run()
189            {
190                listener.containingPageDidAttach();
191            }
192        });
193
194        addPageDetachedCallback(new Runnable()
195        {
196            public void run()
197            {
198                listener.containingPageDidDetach();
199            }
200        });
201    }
202
203    public void removeLifecycleListener(PageLifecycleListener listener)
204    {
205        lifecycleListenersLock.check();
206
207        throw new UnsupportedOperationException("It is not longer possible to remove a page lifecycle listener; please convert your code to use the addPageLoadedCallback() method instead.");
208    }
209
210    public boolean detached()
211    {
212        boolean result = false;
213
214        for (Runnable callback : detachCallbacks)
215        {
216            try
217            {
218                callback.run();
219            } catch (RuntimeException ex)
220            {
221                result = true;
222
223                getLogger().error(String.format("Callback %s failed during page detach: %s", callback, ExceptionUtils.toMessage(ex)), ex);
224            }
225        }
226
227        return result;
228    }
229
230    public void loaded()
231    {
232        lifecycleListenersLock.lock();
233
234        invokeCallbacks(loadedCallbacks);
235
236        loadedCallbacks = null;
237
238        verifyListenerLocks.lock();
239
240        invokeCallbacks(pageVerifyCallbacks);
241
242        pageVerifyCallbacks = null;
243
244        loadComplete = true;
245    }
246
247    public void attached()
248    {
249        attachCount.incrementAndGet();
250
251        invokeCallbacks(attachCallbacks);
252    }
253
254    public Logger getLogger()
255    {
256        return rootElement.getLogger();
257    }
258
259    public void persistFieldChange(ComponentResources resources, String fieldName, Object newValue)
260    {
261        if (!loadComplete)
262        {
263            throw new RuntimeException(StructureMessages.persistChangeBeforeLoadComplete());
264        }
265
266        persistentFieldManager.postChange(name, resources, fieldName, newValue);
267    }
268
269    public Object getFieldChange(String nestedId, String fieldName)
270    {
271        return fieldBundle.createObject().getValue(nestedId, fieldName);
272    }
273
274    public void discardPersistentFieldChanges()
275    {
276        persistentFieldManager.discardChanges(name);
277    }
278
279    public String getName()
280    {
281        return name;
282    }
283
284    public void addResetCallback(Runnable callback)
285    {
286        assert callback != null;
287
288        lifecycleListenersLock.check();
289
290        resetCallbacks.add(callback);
291    }
292
293    public void addResetListener(final PageResetListener listener)
294    {
295        assert listener != null;
296
297        addResetCallback(new Runnable()
298        {
299            public void run()
300            {
301                listener.containingPageDidReset();
302            }
303        });
304    }
305
306    public void addVerifyCallback(Runnable callback)
307    {
308        verifyListenerLocks.check();
309
310        assert callback != null;
311
312        pageVerifyCallbacks.add(callback);
313    }
314
315    public void pageReset()
316    {
317        invokeCallbacks(resetCallbacks);
318    }
319
320    public boolean hasResetListeners()
321    {
322        return !resetCallbacks.isEmpty();
323    }
324
325    public int getAttachCount()
326    {
327        return attachCount.get();
328    }
329
330    public boolean isExactParameterCountMatch()
331    {
332        return exactParameterCountMatch;
333    }
334
335    public void addPageLoadedCallback(Runnable callback)
336    {
337        lifecycleListenersLock.check();
338
339        assert callback != null;
340
341        loadedCallbacks.add(callback);
342    }
343
344    public void addPageAttachedCallback(Runnable callback)
345    {
346        lifecycleListenersLock.check();
347
348        assert callback != null;
349
350        attachCallbacks.add(callback);
351    }
352
353    public void addPageDetachedCallback(Runnable callback)
354    {
355        lifecycleListenersLock.check();
356
357        assert callback != null;
358
359        detachCallbacks.add(callback);
360    }
361
362    private void invokeCallbacks(List<Runnable> callbacks)
363    {
364        for (Runnable callback : callbacks)
365        {
366            callback.run();
367        }
368    }
369}