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}