001/**
002 * Copyright 2010 The Kuali Foundation Licensed under the
003 * Educational Community License, Version 2.0 (the "License"); you may
004 * not use this file except in compliance with the License. You may
005 * obtain a copy of the License at
006 *
007 * http://www.osedu.org/licenses/ECL-2.0
008 *
009 * Unless required by applicable law or agreed to in writing,
010 * software distributed under the License is distributed on an "AS IS"
011 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
012 * or implied. See the License for the specific language governing
013 * permissions and limitations under the License.
014 */
015
016/**
017 *
018 */
019package org.kuali.student.common.ui.client.mvc;
020
021import java.util.Date;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.kuali.student.common.ui.client.configurable.mvc.FieldDescriptor;
028import org.kuali.student.common.ui.client.mvc.ModelChangeEvent.Action;
029import org.kuali.student.common.ui.client.validator.ClientDateParser;
030import org.kuali.student.common.ui.client.validator.DataModelValidator;
031import org.kuali.student.r1.common.assembly.data.Data;
032import org.kuali.student.r1.common.assembly.data.Metadata;
033import org.kuali.student.r1.common.assembly.data.ModelDefinition;
034import org.kuali.student.r1.common.assembly.data.QueryPath;
035import org.kuali.student.r1.common.assembly.data.Data.DataType;
036import org.kuali.student.r1.common.assembly.data.Data.DataValue;
037import org.kuali.student.r1.common.assembly.data.Data.Key;
038import org.kuali.student.r1.common.assembly.data.Data.Property;
039import org.kuali.student.r1.common.assembly.data.Data.Value;
040import org.kuali.student.r1.common.assembly.data.HasChangeCallbacks.ChangeCallback;
041import org.kuali.student.r1.common.assembly.data.HasChangeCallbacks.ChangeCallbackRegistration;
042import org.kuali.student.r1.common.assembly.data.HasChangeCallbacks.ChangeType;
043import org.kuali.student.r2.common.dto.ValidationResultInfo;
044import org.kuali.student.r1.common.validator.DateParser;
045
046import com.google.gwt.core.client.GWT;
047import com.google.gwt.event.shared.HandlerManager;
048import com.google.gwt.event.shared.HandlerRegistration;
049
050/**
051 * The data model for Kuali Student.  Data is stored in a map of maps and is accessed through a QueryPath.
052 * 
053 * @author Kuali Student Team
054 * @see QueryPath
055 */
056@SuppressWarnings("unchecked")
057public class DataModel implements Model {
058    public interface QueryCallback<T> {
059        void onResult(QueryPath path, T result);
060    }
061
062    private static final long serialVersionUID = 1L;
063
064    private String modelName = "";
065    private ModelDefinition definition;
066    private DataModelValidator validator = new DataModelValidator();
067
068    private HandlerManager handlers = new HandlerManager(this);
069    private ChangeCallbackRegistration bridgeCallbackReg;
070
071    private Data root;
072
073    private String parentPath;    //Set this if DataModel's root element is nested in another data element.
074
075    public DataModel() {
076        // do nothing
077    }
078
079    public DataModel(String name) {
080        this.modelName = name;
081    }
082
083    public DataModel(final ModelDefinition definition, final Data root) {
084        this.definition = definition;
085        this.root = root;
086        validator.setDateParser((DateParser) GWT.create(ClientDateParser.class));
087    }
088
089    public String getModelName() {
090        return modelName;
091    }
092
093    public void setModelName(String modelName) {
094        this.modelName = modelName;
095    }
096
097    public <T> T get(final QueryPath path) {
098        return (T) root.query(path);
099    }
100
101    public <T> T get(final String path) {
102        return (T) get(QueryPath.parse(path));
103    }
104
105    /**
106     * @return the root
107     */
108    public Data getRoot() {
109        return root;
110    }
111
112    public void resetRoot() {
113        root = null;
114    }
115
116    public void remove(final QueryPath path) {
117        QueryPath parent = null;
118        QueryPath leavePath = null;
119        if (path != null && path.size() >= 2) {
120            parent = path.subPath(0, path.size() - 1);
121            leavePath = path.subPath(path.size() - 1, path.size());
122            Object parentData = this.get(parent);
123            if (parentData != null && parentData instanceof Data) {
124                ((Data) parentData).remove(
125                        new Data.StringKey(leavePath.toString()));
126            }
127
128        } else if (path != null) {
129            root.remove(new Data.StringKey(path.toString()));
130        }
131    }
132
133    /** 
134     * @param path The path in the data model
135     * @return A map containing the path/value pairs for all matching elements, or an empty
136     * map if no matching values found.
137     */
138    public Map<QueryPath, Object> query(final QueryPath path) {
139        Map<QueryPath, Object> result = new HashMap<QueryPath, Object>();
140        queryRelative(root, path, result);
141        return result;
142    }
143
144    /** 
145     * @param path The path in the data model
146     * @return A map containing the path/value pairs for all matching elements, or an empty
147     * map if no matching values found.
148     */
149    public Map<QueryPath, Object> query(final String path) {
150        return query(QueryPath.parse(path));
151    }
152
153    /**
154     * @param branch The data branch on which to perform the query
155     * @param path   The path string relative to the root of the branch
156     * @param result A map containing all matching paths and the corresponding data value. For a non-wildcard path
157     *               this should return a single entry in the map of the path supplied and the corresponding value. For
158     *               a path containing wildcard, an entry in the map will exist for each matching path and their corresponding values.
159     *               Note: A path ending in a wild card will not match the _runtimeData element.
160     */
161    private void queryRelative(final Data branch, final QueryPath path, Map<QueryPath, Object> result) {
162        //Should this add the entire branch to the result when query path is an empty string?
163
164        Data d = branch;
165
166        for (int i = 0; i < path.size(); i++) {
167            if (d == null) {
168                // dead end
169                break;
170            }
171            final Key key = path.get(i);
172            if (key.equals(Data.WILDCARD_KEY)) {
173                final QueryPath relative = path.subPath(i + 1, path.size());
174                if (!relative.isEmpty()) {
175                    //Handle remaining path after wildcard
176                    for (final Property p : d) {
177                        if (p.getValueType().equals(Data.class)) {
178                            queryRelative((Data) p.getValue(), relative, result);
179                        }
180                    }
181                } else {
182                    //The wildcard is last element in path, so add all sub-elements to result
183                    //The wildcard will not match a _runtimeData path
184                    Set<Key> keys = d.keySet();
185                    for (Key wildcardKey : keys) {
186                        if (!("_runtimeData".equals(wildcardKey.get()))) {
187                            QueryPath wildcardPath = path.subPath(0, path.size() - 1);
188                            wildcardPath.add(wildcardKey);
189                            result.put(wildcardPath, d.get(wildcardKey));
190                        }
191                    }
192                }
193                break; //No need to continue once we process wildcard
194            } else if (i < path.size() - 1) {
195                d = d.get(key);
196            } else {
197                final QueryPath resultPath = d.getQueryPath();
198                resultPath.add(key);
199
200                Object resultValue = d.get(key);
201
202                //If query is against DataModel whose root element is child of another data object, 
203                //need to strip of the parent path so result path is relative to root of child element
204                if (parentPath != null) {
205                    String relativePath = resultPath.toString();
206                    if (relativePath.contains("/")) {
207                        relativePath = relativePath.substring(parentPath.length());
208                        result.put(QueryPath.parse(relativePath), resultValue);
209                    }else{
210                       result.put(resultPath, resultValue); 
211                    }
212                } else {
213                    result.put(resultPath, resultValue);
214                }
215            }
216        }
217    }
218
219    public void set(final QueryPath path, final Data value) {
220        set(path, new Data.DataValue(value));
221    }
222
223    public void set(final QueryPath path, final Integer value) {
224        set(path, new Data.IntegerValue(value));
225    }
226
227    public void set(final QueryPath path, final String value) {
228        set(path, new Data.StringValue(value));
229    }
230
231    public void set(final QueryPath path, final Long value) {
232        set(path, new Data.LongValue(value));
233    }
234
235    public void set(final QueryPath path, final Short value) {
236        set(path, new Data.ShortValue(value));
237    }
238
239    public void set(final QueryPath path, final Double value) {
240        set(path, new Data.DoubleValue(value));
241    }
242
243    public void set(final QueryPath path, final Float value) {
244        set(path, new Data.FloatValue(value));
245    }
246
247    public void set(final QueryPath path, final Boolean value) {
248        set(path, new Data.BooleanValue(value));
249    }
250
251    public void set(final QueryPath path, final Date value) {
252        set(path, new Data.DateValue(value));
253    }
254
255    public void set(final QueryPath path, final Value value) {
256        definition.ensurePath(root, path, value instanceof DataValue);
257        if (path.size() > 1) {
258            final QueryPath q = path.subPath(0, path.size() - 1);
259            final Data d = root.query(q);
260            d.set(path.get(path.size() - 1), value);
261        } else {
262            root.set(path.get(0), value);
263        }
264    }
265
266    public DataType getType(final QueryPath path) {
267        return definition.getType(path);
268    }
269
270    public Metadata getMetadata(final QueryPath path) {
271        return definition.getMetadata(path);
272    }
273
274    /**
275     * Set the top level data for this DataModel
276     * @param root the root to set
277     */
278    public void setRoot(final Data root) {
279        if (bridgeCallbackReg != null) {
280            bridgeCallbackReg.remove();
281        }
282        this.root = root;
283        bridgeCallbackReg = root.addChangeCallback(new ChangeCallback() {
284            @Override
285            public void onChange(ChangeType type, QueryPath path) {
286                Action action = null;
287                if (type == ChangeType.ADD) {
288                    action = Action.ADD;
289                } else if (type == ChangeType.REMOVE) {
290                    action = Action.REMOVE;
291                } else if (type == ChangeType.UPDATE) {
292                    action = Action.UPDATE;
293                }
294                handlers.fireEvent(new DataModelChangeEvent(action, DataModel.this, path));
295            }
296        });
297        handlers.fireEvent(new DataModelChangeEvent(Action.RELOAD, this, new QueryPath()));
298    }
299
300    @Override
301    public HandlerRegistration addModelChangeHandler(ModelChangeHandler handler) {
302        return handlers.addHandler(ModelChangeEvent.TYPE, handler);
303    }
304
305    public ModelDefinition getDefinition() {
306        return definition;
307    }
308
309    public void setDefinition(ModelDefinition definition) {
310        this.definition = definition;
311    }
312
313
314    public String getParentPath() {
315        return parentPath;
316    }
317
318
319    /**
320     * If the root element for this is a child of another data object, then the parent
321     * path must be set to the path where this child data object can be found.
322     *
323     * @param parentPath
324     */
325    public void setParentPath(String parentPath) {
326        this.parentPath = parentPath;
327    }
328
329    /**
330     * Validates this data model against its ModelDefinition/Metadata and returns the result
331     * to the callback
332     * @param callback
333     */
334    public void validate(final Callback<List<ValidationResultInfo>> callback) {
335        List<ValidationResultInfo> result = validator.validate(this);
336        callback.exec(result);
337    }
338
339    /**
340     * Validates this data model against the next state in its ModelDefinition and returns the result
341     * to the callback
342     * @param callback
343     */
344    public void validateNextState(final Callback<List<ValidationResultInfo>> callback) {
345        List<ValidationResultInfo> result = validator.validateNextState(this);  // loads missingField result info 
346        callback.exec(result);
347    }
348
349    /**
350     * Validates this data model against the given metadata and returns the result
351     * to the callback
352     * @param metadata
353     * @param callback
354     */
355    public void validateForMetadata(Metadata metadata, final Callback<List<ValidationResultInfo>> callback) {
356        List<ValidationResultInfo> result = validator.validateForMetadata(metadata, this);
357        callback.exec(result);
358    }
359    
360    /**
361     * Validates a single field
362     * @param fd
363     * @param callback
364     */
365    public void validateField(FieldDescriptor fd, final Callback<List<ValidationResultInfo>> callback) {
366        List<ValidationResultInfo> result = validator.validate(fd, this);
367        callback.exec(result);
368    }
369
370    /**
371     * Checks to see if data exists for the path passed in
372     * @param sPath
373     * @return
374     */
375    public boolean isValidPath(String sPath) {
376        QueryPath path = QueryPath.parse(sPath);
377        boolean result = false;
378        Data root = this.getRoot();
379        for (int i = 0; i < path.size(); i++) {
380            Data.Key key = path.get(i);
381            if (!root.containsKey(key)) {
382                result = false;
383                break;
384            } else if (i < path.size() - 1) {
385                root = (Data) root.get(key);
386            } else {
387                result = true;
388                break;
389            }
390        }
391        return result;
392    }
393
394}