001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2012 GRANITE DATA SERVICES S.A.S.
004    
005      This file is part of Granite Data Services.
006    
007      Granite Data Services is free software; you can redistribute it and/or modify
008      it under the terms of the GNU Library General Public License as published by
009      the Free Software Foundation; either version 2 of the License, or (at your
010      option) any later version.
011    
012      Granite Data Services is distributed in the hope that it will be useful, but
013      WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014      FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015      for more details.
016    
017      You should have received a copy of the GNU Library General Public License
018      along with this library; if not, see <http://www.gnu.org/licenses/>.
019    */
020    
021    package org.granite.client.tide.server;
022    
023    import java.util.ArrayList;
024    import java.util.HashMap;
025    import java.util.List;
026    import java.util.Map;
027    
028    import org.granite.client.tide.ScopeType;
029    import org.granite.client.tide.SyncMode;
030    import org.granite.logging.Logger;
031    import org.granite.tide.invocation.ContextResult;
032    import org.granite.tide.invocation.ContextUpdate;
033    
034    /**
035     * @author William DRAI
036     */
037    public class TrackingContext {
038        
039        private static final Logger log = Logger.getLogger(TrackingContext.class);
040        
041        private boolean enabled = true;
042        private List<ContextUpdate> updates = new ArrayList<ContextUpdate>();
043        private List<ContextUpdate> pendingUpdates = new ArrayList<ContextUpdate>();
044        private List<ContextResult> results = new ArrayList<ContextResult>();
045        private List<String> lastResults = new ArrayList<String>();
046    
047        /**
048         *  @private
049         *  Enable/disable tracking on the context
050         * 
051         *  @param enabled enable or disable tracking on the current context
052         */
053        public void setEnabled(boolean enabled) {
054            this.enabled = enabled;
055        }
056        
057        /**
058         *  @private
059         *  Return tracking mode
060         * 
061         *  @return true if tracking enabled
062         */
063        public boolean isEnabled() {
064            return enabled;
065        }
066        
067        /**
068         *  @private
069         *  @return current list of updates that will be sent to the server
070         */
071        public List<ContextUpdate> getUpdates() {
072            return updates;
073        }
074        
075        /**
076         *  @private
077         *  @return current list of results that will be requested from the server
078         */
079        public List<ContextResult> getResults() {
080            return results;
081        }       
082        
083        /**
084         *  @private
085         *  Resets the context
086         */
087        public void clear() {
088            updates.clear();
089            pendingUpdates.clear();
090            results.clear();
091            lastResults.clear();
092        }
093        
094        /**
095         *  Resets the current updates and optionally saves them as pending
096         * 
097         *  @param savePending moves existing updates to pending before clearing
098         */
099        public void clearUpdates(boolean savePending) {
100            if (savePending)
101                pendingUpdates = new ArrayList<ContextUpdate>(updates);
102            updates.clear();
103        }
104        
105        /**
106         *  Resets the current pending updates
107         */
108        public void clearPendingUpdates() {
109            pendingUpdates.clear();
110            lastResults.clear();
111        }
112        
113        /**
114         *  Filter the current updates with a function
115         */
116        public void filterUpdates(UpdateFilter filter) {           
117            // Keep only updates for identity component
118            for (int i = 0; i < updates.size(); i++) {
119                ContextUpdate u = updates.get(i);
120                if (!filter.accept(u)) {
121                    updates.remove(i);
122                    i--;
123                }
124            }
125        }
126            
127        public static interface UpdateFilter {        
128            public boolean accept(ContextUpdate u);        
129        }
130        
131        
132        /**
133         *  @private
134         *  Add update to current context 
135         *  Note: always move the update in last position in case the value can depend on previous updates
136         * 
137         *  @param componentName name of the component/context variable or null if typed component reference
138         *  @param componentClassName class name of the component/context variable or null if untyped component name
139         *  @param expr EL expression to evaluate
140         *  @param value value to send to server
141         *  @param typed component name represents a typed component instance  
142         */
143        public void addUpdate(String componentName, String componentClassName, String expr, Object value) {
144            internalAddUpdate(componentName, componentClassName, expr, value, ScopeType.EVENT, SyncMode.NONE, componentName == null);             
145        }
146        
147        /**
148         *  @private
149         *  Add update to current context 
150         *  Note: always move the update in last position in case the value can depend on previous updates
151         * 
152         *  @param componentName name of the component/context variable or null if typed component reference
153         *  @param componentClassName class name of the component/context variable or null if untyped component name
154         *  @param expr EL expression to evaluate
155         *  @param value value to send to server
156         *  @param scope scope of the result
157         *  @param sync remote sync mode of the result
158         *  @param typed component name represents a typed component instance  
159         */
160        protected void internalAddUpdate(String componentName, String componentClassName, String expr, Object value, ScopeType scope, SyncMode sync, boolean typed) {
161            if (!enabled)
162                return;
163            
164            boolean found = false;
165            for (int i = 0; i < updates.size(); i++) {
166                ContextUpdate u = updates.get(i);
167                if (u.getComponentName() == componentName && u.getComponentClassName() == componentClassName && u.getExpression() == expr) {
168                    u.setValue(value);
169                    if (i < updates.size()-1) {
170                        found = false;
171                        updates.remove(i);    // Remove here to add it in last position
172                        i--;
173                    }
174                    else
175                        found = true;
176                }
177                else if (u.getComponentName() == componentName && u.getComponentClassName() == componentClassName && u.getExpression() != null && (expr == null || u.getExpression().indexOf(expr + ".") == 0)) {
178                    updates.remove(i);
179                    i--;
180                }
181                else if (u.getComponentName() == componentName && u.getComponentClassName() == componentClassName && expr != null && (u.getExpression() == null || expr.indexOf(u.getExpression() + ".") == 0))
182                    found = true;
183            }
184            
185            if (!found) {
186                log.debug("add new update {0}", (componentName != null ? componentName : "") + (componentClassName != null ? "(" + componentClassName + ")" : "") + (expr != null ? "." + expr : ""));
187                ContextUpdate cu = new ContextUpdate(componentName, expr, value, scope.ordinal(), false);
188                cu.setComponentClassName(componentClassName);
189                updates.add(cu);
190            }
191        }
192        
193        /**
194         *  @private
195         *  Add result evaluator in current context
196         * 
197         *  @param componentName name of the component/context variable
198         *  @param componentClassName class name of the component/context variable
199         *  @param expr EL expression to evaluate
200         *  @param instance current instance of the component
201         * 
202         *  @return true if the result was not already present in the current context 
203         */
204        public boolean addResult(String componentName, String componentClassName, String expr, Object instance) {
205            return internalAddResult(componentName, componentClassName, expr, instance, ScopeType.EVENT, SyncMode.NONE);
206        }
207        
208        /**
209         *  @private
210         *  Add result evaluator in current context
211         * 
212         *  @param componentName name of the component/context variable
213         *  @param componentClassName class name of the component/context variable
214         *  @param expr EL expression to evaluate
215         *  @param instance current instance of the component
216         *  @param scope scope of the update
217         *  @param sync remote sync mode of the update
218         * 
219         *  @return true if the result was not already present in the current context 
220         */
221        protected boolean internalAddResult(String componentName, String componentClassName, String expr, Object instance, ScopeType scope, SyncMode sync) {
222            if (!enabled || sync == SyncMode.NONE || (instance == null && expr == null))
223                return false;
224            
225            // Check in existing results
226            for (ContextResult r : results) {
227                if (r.getComponentName() == componentName && r.getComponentClassName() == componentClassName && r.getExpression() == expr)
228                    return false;
229            }
230            
231            // Check in last received results
232            String e = componentName + (componentClassName != null ? "(" + componentClassName + ")" : "") + (expr != null ? "." + expr : "");
233            if (lastResults.indexOf(e) >= 0)
234                return false;
235            
236            log.debug("add new result {0}", e);
237            // TODO: should store somewhere if the client componentName is the same as the server bean name
238            ContextResult cr = new ContextResult(componentName, expr);
239            cr.setComponentClassName(componentClassName);
240            results.add(cr);
241            return true;
242        }
243        
244        
245        public void addLastResult(String res) {
246            lastResults.add(res);
247        }
248        
249        
250        public void removeResults(List<ContextUpdate> rmap) {
251            // Remove all received results from current results list
252            List<ContextResult> newResults = new ArrayList<ContextResult>();
253            for (ContextResult cr : this.results) {
254                boolean found = false;
255                for (ContextUpdate u : rmap) {
256                    if (cr.matches(u.getComponentName(), u.getComponentClassName(), u.getExpression())) {
257                        found = true;
258                        break;
259                    }
260                }
261                if (!found)
262                    newResults.add(cr);
263            }
264            this.results = newResults;
265        }
266        
267        
268        /**
269         *  Trace the current tracking context
270         */
271        public void traceContext() {
272            log.debug("updates: %s", updates.toString());
273            log.debug("results: %s", results.toString());
274        }
275        
276        
277        /**
278         *  @private
279         * 
280         *  Reset current tracking context and returns saved context
281         * 
282         *  @return saved tracking context
283         */
284        public Map<String, Object> saveAndResetContext() {
285            Map<String, Object> savedTrackingContext = new HashMap<String, Object>();
286            savedTrackingContext.put("updates", new ArrayList<ContextUpdate>(updates));
287            savedTrackingContext.put("results", new ArrayList<ContextResult>(results));
288            savedTrackingContext.put("pendingUpdates", new ArrayList<ContextUpdate>(pendingUpdates));
289            savedTrackingContext.put("lastResults", new ArrayList<String>(lastResults));
290            
291            updates.clear();
292            pendingUpdates.clear();
293            results.clear();
294            lastResults.clear();
295            
296            return savedTrackingContext;
297        }
298        
299        /**
300         *  @private
301         * 
302         *  Restore tracking context
303         * 
304         *  @param trackingContext object containing the current call context
305         */ 
306        @SuppressWarnings("unchecked")
307        public void restoreContext(Map<String, Object> trackingContext) {
308            updates = (List<ContextUpdate>)trackingContext.get("updates");
309            results = (List<ContextResult>)trackingContext.get("results");
310            pendingUpdates = (List<ContextUpdate>)trackingContext.get("pendingUpdates");
311            lastResults = (List<String>)trackingContext.get("lastResults");
312        }
313    }