001    /*
002     * Copyright © 2008, 2012 Pedro Agulló Soliveres.
003     * 
004     * This file is part of DirectJNgine.
005     *
006     * DirectJNgine is free software: you can redistribute it and/or modify
007     * it under the terms of the GNU Lesser General Public License as published by
008     * the Free Software Foundation, either version 3 of the License.
009     *
010     * Commercial use is permitted to the extent that the code/component(s)
011     * do NOT become part of another Open Source or Commercially developed
012     * licensed development library or toolkit without explicit permission.
013     *
014     * DirectJNgine is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017     * GNU Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public License
020     * along with DirectJNgine.  If not, see <http://www.gnu.org/licenses/>.
021     * 
022     * This software uses the ExtJs library (http://extjs.com), which is 
023     * distributed under the GPL v3 license (see http://extjs.com/license).
024     */
025    
026    package com.softwarementors.extjs.djn.gson;
027    
028    import java.util.Collection;
029    import java.util.Collections;
030    import java.util.HashSet;
031    import java.util.IdentityHashMap;
032    import java.util.Map;
033    import java.util.Set;
034    import java.util.Stack;
035    
036    import com.softwarementors.extjs.djn.ClassUtils;
037    import com.softwarementors.extjs.djn.CollectionUtils;
038    import com.softwarementors.extjs.djn.StringUtils;
039    
040    import edu.umd.cs.findbugs.annotations.NonNull;
041    
042    public class JsonDeserializationManager {
043      private static @NonNull ThreadLocal<JsonDeserializationManager> manager = new ThreadLocal<JsonDeserializationManager>();
044      private static @NonNull Set<Class<?>> manyValuedClasses = new HashSet<Class<?>>();
045      
046      static {
047        manyValuedClasses.add( Collection.class );
048      }
049      
050      public static void registerManyValuedClasses( Class<?> clazz, Class<?>[] otherClasses) {
051        assert clazz != null;
052        assert otherClasses != null;
053        
054        manyValuedClasses.add(clazz);
055        Collections.addAll( manyValuedClasses, otherClasses );
056      }
057      
058      JsonDeserializationManager() {
059        // Avoid instantiation
060      }
061    
062      private @NonNull Stack<Object> parents = new Stack<Object>();
063      private @NonNull Stack<String> fields = new Stack<String>();
064      private @NonNull Map<Object,Set<String>> fieldExclusions = new IdentityHashMap<Object,Set<String>>();
065      private @NonNull Set<String> rootExclusions = new HashSet<String>();
066      private @NonNull Set<String> rootInclusions = new HashSet<String>();
067      private Object root;
068      private boolean excludeManyValuedFields;
069      
070    
071      public static JsonDeserializationManager getManager() {
072        if( manager.get() == null ) {
073          manager.set(new JsonDeserializationManager() );
074        }
075        return manager.get();
076      }
077    
078      public void friendOnlyAccess_pushField(String name) {
079        assert !StringUtils.isEmpty(name);
080        
081        this.fields.push(name);
082      }
083    
084      public void friendOnlyAccess_popField() {
085        this.fields.pop();
086      }
087    
088      public void friendOnlyAccess_pushParent(Object obj) {
089        assert obj != null;
090        
091        this.parents.push(obj);
092      }
093    
094      public void friendOnlyAccess_popParent() {
095        this.parents.pop();
096      }
097      
098      public void excludeManyValuedFields() {
099        this.excludeManyValuedFields = true;
100      }
101    
102      public void excludeFieldPaths(String firstFieldPath, String... fieldPaths) {
103        assert this.rootExclusions != null;
104        assert firstFieldPath != null;
105        assert fieldPaths != null;
106        
107        this.rootInclusions.remove(firstFieldPath);
108        CollectionUtils.removeAll( this.rootInclusions, fieldPaths );
109        this.rootExclusions.add( firstFieldPath );
110        Collections.addAll(this.rootExclusions, fieldPaths);
111      }
112      
113      public void includeFieldPaths(String firstFieldPath, String... fieldPaths) {
114        assert this.rootInclusions != null;
115        assert firstFieldPath != null;
116        assert fieldPaths != null;
117        
118        this.rootExclusions.remove(firstFieldPath);
119        CollectionUtils.removeAll( this.rootExclusions, fieldPaths );
120        this.rootInclusions.add( firstFieldPath );
121        Collections.addAll(this.rootInclusions, fieldPaths);
122      }
123    
124      /*
125      public void excludeObjectFieldPaths(Object t, String firstFieldPath, String... fieldPaths) {
126        if( t == null ) {
127          return;
128        }
129        
130        assert firstFieldPath != null;
131        assert fieldPaths != null;
132    
133        Set<String> exclusions = this.fieldExclusions.get(t);
134        if( exclusions == null ) {
135          exclusions = new HashSet<String>();
136          this.fieldExclusions.put( t, exclusions );
137        }
138        exclusions.add(firstFieldPath);
139        Collections.addAll( exclusions, fieldPaths);
140      }
141      */
142    
143      public boolean friendOnlyAccess_isFieldExcluded(Object value, String field) {
144        assert value != null;
145        assert field != null;
146        
147        if( isRootManyValuedFieldExcluded(value, field)) {
148          return true;
149        }
150        
151        if( this.fieldExclusions.isEmpty()) {
152          return false;
153        }
154        
155        // value is an special case: can't call hasExclusionForField
156        // because it checks the parents list.
157        // However, check is trivial, because if it has field, it
158        // must be as is, not as a dotted path!
159        if( this.fieldExclusions.containsKey(value)) {
160          Set<String> ex = this.fieldExclusions.get(value);
161          if( ex.contains(field)) {
162            return true;
163          }
164        }
165    
166        for( Object obj : this.parents) {
167          if( this.fieldExclusions.containsKey(obj)) {
168            boolean exclude = isFieldExcludedByObjectInParentsChain(obj, field); 
169            if( exclude ) {
170              return true;
171            }
172          }
173        }
174        return false;
175      }
176    
177      private boolean isRootManyValuedFieldExcluded(Object value, String field) {
178        assert value != null;
179        
180        if( !this.excludeManyValuedFields || value != this.root ) {
181          return false;
182        }
183        
184        // Need to check all public and private fields
185        // -maybe in parent classes too
186        Class<?> fieldType = ClassUtils.getFieldType(value.getClass(), field);
187        if( fieldType == null )
188          return false;
189        return isManyValuedClass(fieldType);
190      }
191    
192      /* package */ boolean isFieldExcludedByObjectInParentsChain(Object obj, String field) {
193        assert obj != null;
194        assert field != null;
195        int positionInChain = this.parents.indexOf(obj);
196        assert positionInChain >= 0;
197    
198        // The last object is a parent or parent of a parent & has fieldExclusions.
199        // We need to check whether the last field is excluded from that object!
200        StringBuilder path = new StringBuilder();
201        for( int i = positionInChain; i < this.fields.size(); i++) {
202          path.append( this.fields.get(i) );
203          path.append( '.' );
204        }
205        path.append( field );
206        Set<String> objectExclusions = this.fieldExclusions.get(obj);
207        assert objectExclusions != null;
208    
209        boolean excluded = objectExclusions.contains(path.toString());
210        return excluded;
211      }
212    
213      public void friendOnlyAccess_setRoot(Object root) {
214        if( root != null ) {
215          this.root = root;
216          this.fieldExclusions.put(root, this.rootExclusions);
217        }
218      }
219    
220      public void friendOnlyAccess_dispose() {
221        manager.remove();
222      }
223    
224      public static boolean isManyValuedClass(Class<?> clazz) {
225        assert clazz != null;
226        if( clazz.isArray() ) {
227          return true;
228        }
229        for( Class<?> manyValuedClass : manyValuedClasses ) {
230          if( manyValuedClass.isAssignableFrom(clazz) )
231            return true;
232        }
233        return false;
234      }
235    
236      /* Why are we not supporting excludeObjects?
237       * 
238       * Because excluded object MUST e to null: due to theway json is parsed by gson,
239       * if you have arrived to the object, you already processed and stored its name, 
240       * and then we MUST do someting with it, namely set it to null (or else,
241       * there will be a parse error.
242       * Now, Gson might remove this object or not depending on its 
243       * 'serializeNull' settings: but, if it does not, we will end up finding that
244       * whereas excluded objects are set to null, excluede properties are simply
245       * missing. This is a hole in semantics I dislike very much.
246       */
247      /*
248      public void excludeObjects( Object firstObject, Object... objects ) {
249         assert objects != null;
250         
251         this.exclusions.put( firstObject, new HashSet<String>() );
252         for( Object obj : objects ) {
253            this.exclusions.put(obj, new HashSet<String>()); 
254         }
255      }
256    
257      package-visible boolean isObjectExcluded(Object value) {
258        Set<String> objectExclusions = this.exclusions.get(value);
259        if( objectExclusions == null ) {
260          return false;
261        }
262        return objectExclusions.isEmpty();
263      }
264      */
265    }