1 /*
  2  *  Copyright (C) 2009 WaveMaker Software, Inc.
  3  *
  4  *  This file is part of the WaveMaker Client Runtime.
  5  *
  6  *  Licensed under the Apache License, Version 2.0 (the "License");
  7  *  you may not use this file except in compliance with the License.
  8  *  You may obtain a copy of the License at
  9  *
 10  *      http://www.apache.org/licenses/LICENSE-2.0
 11  *
 12  *  Unless required by applicable law or agreed to in writing, software
 13  *  distributed under the License is distributed on an "AS IS" BASIS,
 14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  *  See the License for the specific language governing permissions and
 16  *  limitations under the License.
 17  */
 18 dojo.provide("wm.base.components.LiveVariable");
 19 dojo.require("wm.base.components.ServiceVariable");
 20 dojo.require("wm.base.components.LiveView");
 21 
 22 /**
 23 	Component that marshalls a LiveView and can perform all data operations: read, insert, update, delete.
 24 	@name wm.LiveVariable
 25 	@class
 26 	@extends wm.ServiceVariable
 27 */
 28 dojo.declare("wm.LiveVariable", wm.ServiceVariable, {
 29 	/**
 30 		@lends wm.LiveVariable.prototype
 31 	*/
 32 	autoUpdate: true,
 33 	startUpdate: true,
 34 	operation: "read",
 35 	/** First row of results */
 36 	firstRow: 0,
 37 	/** Optional starting source data */
 38 	sourceData: null,
 39 	/** Method by which data is filtered */
 40 	matchMode: "start",
 41 	/** Toggle for data sorting to ignore alphabetical case or not. */
 42 	ignoreCase: true,
 43 	/** Optional order by clause, example "asc: cityId, desc: city" */
 44 	orderBy: "",
 45 	/** LiveView or LiveTable from which this LiveVariable gets its field information; can use a liveView or liveTable */
 46 	liveSource: null,
 47 	/** our liveView **/
 48 	liveView: null,
 49 	/** Maximum number of results to return */
 50 	maxResults: 0,
 51 	designMaxResults: 500,
 52 	/** Field in view to use as our root object / type */
 53 	_rootField: "",
 54 	destroy: function() {
 55 		this._unsubscribeLiveView();
 56 		this.inherited(arguments);
 57 	},
 58 	init: function() {
 59 		this.inherited(arguments);
 60 		this.filter = new wm.Variable({name: "filter", owner: this, type: this.type || "any" });
 61 		this.sourceData = new wm.Variable({name: "sourceData", owner: this, type: this.type || "any" });
 62 		this.subscribe(this.filter.getRuntimeId() + "-changed", this, "filterChanged");
 63 		this.subscribe(this.sourceData.getRuntimeId() + "-changed", this, "sourceDataChanged");
 64 	},
 65 	postInit: function() {
 66 		this.inherited(arguments);
 67 		// initialize via liveSource or optionally directly with a liveView)
 68 		if (this.liveSource)
 69 			this.setLiveSource(this.liveSource);
 70 		else
 71 			this.setLiveView(this.liveView || this.createLiveView(this.type));
 72 		this.doAutoUpdate();
 73 	},
 74 	_subscribeLiveView: function() {
 75 		this._unsubscribeLiveView();
 76 		if (this.liveView)
 77 			this._liveViewSubscription = dojo.subscribe(this.liveView.getRuntimeId() + "-viewChanged", dojo.hitch(this, "_liveViewChanged"));
 78 	},
 79 	_unsubscribeLiveView: function() {
 80 		dojo.unsubscribe(this._liveViewSubscription);
 81 		this._liveViewSubscription = null;
 82 	},
 83 	isLiveType: function() {
 84 		return wm.typeManager.getLiveService(this.type);
 85 	},
 86 	doAutoUpdate: function() {
 87 		if (this.isLiveType())
 88 			this.inherited(arguments);
 89 	},
 90 	filterChanged: function() {
 91 		this.doAutoUpdate();
 92 	},
 93 	sourceDataChanged: function() {
 94 		this.doAutoUpdate();
 95 	},
 96 	/** Set the filter used for read operations */
 97 	setFilter: function(inFilter) {
 98 		if ((inFilter || 0).type == this.type) {
 99 			this.filter.setDataSet(inFilter);
100 		}
101 	},
102 	/** Set the orderBy property used for read operations */
103 	setOrderBy: function(inOrderBy) {
104 		this.orderBy = inOrderBy;
105 		this.doAutoUpdate();
106 	},
107 	/** Set the source data which is used for operations. */
108 	setSourceData: function(inSourceData) {
109 		var liveType = this.isLiveType();
110 		if (!liveType || (inSourceData || 0).type == this.type) {
111 			this.sourceData.setDataSet(inSourceData);
112 			if (!liveType) {
113 				this._updating++;
114 				this.setLiveSource(this.sourceData.type);
115 				this._updating--;
116 			}
117 		}
118 	},
119 	// ==========================================================
120 	// LiveView integration
121 	// ==========================================================
122 	/** Set the LiveView or LiveTable from which we will get data information */
123 	/* valid input: LiveView full id or LiveTable full name */
124 	setLiveSource: function(inLiveSource) {
125 		var
126 			s =this.liveSource = inLiveSource,
127 			v = this.getRoot().app.getValueById(s) || this.createLiveView(s);
128 		if (v)
129 			this.setLiveView(v);
130 		this.doAutoUpdate();
131 	},
132 	setLiveView: function(inLiveView) {
133 		this.clearData();
134 		this.liveView = inLiveView;
135 		this._subscribeLiveView();
136 		this.setType(this.getViewType());
137 	},
138 	createLiveView: function(inType) {
139 		return new wm.LiveView({
140 			name: "liveView",
141 			owner: this,
142 			dataType: inType,
143 			_defaultView: true
144 		});
145 	},
146 	setType: function(inType) {
147 		this.inherited(arguments);
148 		this.filter.setType(this.type);
149 		this.sourceData.setType(this.type);
150 		if (!this._updating && this.$.binding)
151 			this.$.binding.refresh();
152 	},
153 	_liveViewChanged: function() {
154 		this.setType(this.liveView.dataType);
155 		if (this.isDesignLoaded())
156 			this.doAutoUpdate();
157 	},
158 	// ==========================================================
159 	// Server I/O
160 	// ==========================================================
161 	_getCanUpdate: function() {
162 		return this.inherited(arguments) &&
163 			!(this.operation == "read" && this._isSourceDataBound() && wm.isEmpty(this.sourceData.getData()) );
164 	},
165 	// FIXME: need to zot this
166 	operationChanged: function() {
167 	},
168 	_update: function() {
169 		// note: runtime service only available when application is deployed
170 		// so must wait until here to set it.
171 		this._service = wm.getRuntimeService(this);
172 		//console.log(this.name, "update");
173 		return this.inherited(arguments);
174 	},
175 	getArgs: function() {
176 		var
177 			d = this.sourceData.getData(),
178 			t = this.sourceData.type || this.type,
179 			s = wm.typeManager.getService(this.type),
180 			args = [s, t, wm.isEmpty(d) ? null : d];
181 		if (this.operation == "read") {
182 			args = args.concat(this._getReadArguments());
183 		}
184 		return args;
185 	},
186 	_getReadArguments: function() {
187 		var
188 			props = {properties: this._getEagerProps(this), filters: this._getFilters(), matchMode: this.matchMode, ignoreCase: this.ignoreCase},
189 			paging = this.orderBy ? {orderBy: (this.orderBy || "").split(",")} : {},
190 			max = this.isDesignLoaded() ? this.designMaxResults : this.maxResults,
191 			results = max ? { maxResults: max, firstResult: this.firstRow } : {};
192 		dojo.mixin(paging, results);
193 		return [props, paging];
194 	},
195 	_getFilters: function() {
196 		return this._getFilterValues(this.filter.getData());
197 	},
198 	_getFilterValues: function(inData, inPrefix) {
199 		var f = [], d, p;
200 		for (var i in inData) {
201 			d = inData[i];
202 			p = (inPrefix ? (inPrefix ||"") + "." : "") + i;
203 			if (dojo.isObject(d) && d !== null)
204 				f = f.concat(this._getFilterValues(d, p));
205 			else if (p !== undefined && d !== undefined)
206 				f.push(p + "=" + d);
207 		}
208 		return f;
209 	},
210 	_isSourceDataBound: function() {
211 		var wires = this.$.binding.wires, w;
212 		for (var i in wires) {
213 			w = wires[i];
214 			if ((w.targetProperty || "").indexOf("sourceData") == 0)
215 				return true;
216 		}
217 	},
218 	processResult: function(inResult) {
219 		this.dataSetCount = this._service.fullResult.dataSetSize;
220 		this.inherited(arguments);
221 	},
222 	//===========================================================================
223 	// Paging
224 	//===========================================================================
225 	/** Return the current data page; only relevant when maxResults is set. */
226 	getPage: function() {
227 		return Math.floor(this.firstRow / (this.maxResults || 1));
228 	},
229 	/** Return the total number of data pages. */
230 	getTotalPages: function() {
231 		return Math.ceil((this.dataSetCount || 1) / (this.maxResults || 1));
232 	},
233 	/** Set and retrieve the current data page.
234 		@param {Number} inPageIndex the page number to set
235 	 */
236 	setPage: function(inPageIndex) {
237 		inPageIndex = Math.max(0, Math.min(this.getTotalPages()-1, inPageIndex));
238 		this.firstRow = inPageIndex * (this.maxResults || 0);
239 		this.update();
240 	},
241 	/** Set and retrieve the next page of data. */
242 	setNextPage: function() {
243 		this.setPage(this.getPage()+1);
244 	},
245 	/** Set and retrieve the previous page of data. */
246 	setPreviousPage: function() {
247 		this.setPage(this.getPage()-1);
248 	},
249 	/** Set and retrieve the first page of data. */
250 	setFirstPage: function() {
251 		this.setPage(0);
252 	},
253 	/** Set and retrieve the last page of data. */
254 	setLastPage: function() {
255 		this.setPage(this.getTotalPages()-1);
256 	}
257 });
258 
259 //===========================================================================
260 // Design Only
261 //===========================================================================
262 
263 wm.Object.extendSchema(wm.LiveVariable, {
264 	update: {ignore: 1, publicEvent: 1},
265 	related: { ignore: 1},
266 	view: { ignore: 1},
267 	service: { ignore: 1},
268 	dataType: { ignore: 1},
269 	operation: { group: "data", order: 0},
270 	input: {ignore: 1},
271 	liveSource: { group: "data", order: 1},
272 	liveView: { ignore: 1},
273 	sourceData: {ignore: 1, group: "data", order: 3, bindTarget: 1, categoryParent: "Properties", categoryProps: {component: "sourceData", inspector: "Data"}},
274 	filter: { ignore: 1, group: "data", order: 5, bindTarget: 1, categoryParent: "Properties", categoryProps: {component: "filter", inspector: "Data"}},
275 	matchMode: {group: "data", order: 10},
276 	firstRow: {group: "data", order: 15},
277 	maxResults: {group: "data", order: 17},
278 	designMaxResults: {group: "data", order: 18},
279 	orderBy: {group: "data", order: 19},
280 	ignoreCase:  {group: "data", order: 20},
281 	configure: { ignore: 1 },
282 	dataSetCount: { ignore: 1 }
283 });
284 
285 wm.LiveVariable.extend({
286 	_operations: ["read", "insert", "update", "delete"],
287 	_matchModes: ["start", "end", "anywhere", "exact"],
288 	listProperties: function() {
289 		var
290 			p = this.inherited(arguments),
291 			r = (this.operation == "read");
292 		p.matchMode.ignore = !r;
293 		p.firstRow.ignore = !r;
294 		p.maxResults.ignore = !r;
295 		p.designMaxResults.ignore = !r;
296 		p.orderBy.ignore = !r;
297 		p.ignoreCase.ignore = !r;
298 		p.filter.bindTarget = r;
299 		p.filter.categoryParent = r ? "Properties" : "";
300 		return p;
301 	},
302 	isListBindable: function() {
303 		return this.operation == "read" ? !(this.sourceData && !wm.isEmpty(this.sourceData.getData())) : false;
304 	},
305 	designCreate: function() {
306 		this.inherited(arguments);
307 		this.subscribe("wmwidget-idchanged", this, "componentNameChanged");
308 		
309 	},
310 	componentNameChanged: function(inOldId, inNewId, inOldRtId, inNewRtId) {
311 		if (inOldId == this.liveSource)
312 			this.setLiveSource(inNewId);
313 	},
314 	set_operation: function(inOperation) {
315 		this.operation = inOperation;
316 		// just a good idea for safety
317 		if (this.isDesignLoaded()) {
318 			// automatically set autoUpdate to true if we're reading, 
319 			// since this is the default anyway, otherwise set to false.
320 			this.setAutoUpdate(inOperation == "read");
321 			if (studio.selected == this)
322 				studio.inspector.inspect(this);
323 		}
324 	},
325 	set_liveSource: function(inLiveSource) {
326 		this.setLiveSource(inLiveSource);
327 		if (this.isDesignLoaded() && studio.selected == this)
328 			studio.inspector.inspect(this);
329 	},
330 	set_sourceData: function(inSourceData) {
331 		this.setSourceData(inSourceData);
332 		if (this.isDesignLoaded() && studio.selected == this)
333 			studio.inspector.inspect(this);
334 	},
335 	set_filter: function(inFilter) {
336 		this.setFilter(inFilter);
337 		if (this.isDesignLoaded() && studio.selected == this)
338 			studio.inspector.inspect(this);
339 	},
340 	checkOrderBy: function(inOrderBy) {
341 		var
342 			orderParts = (inOrderBy || "").split(','),
343 			re = new RegExp("^(?:asc|desc)\:", "i");
344 		for (var i=0, o; (o = orderParts[i]); i++)
345 			if (!dojo.trim(o).match(re)) {
346 				alert("Each property used in the orderBy clause must be of the form asc|desc: <propertyPath>. \"" + o + "\" does not match this format." + 
347 					" The current orderBy clause will generate an error and should be corrected.");
348 				return;
349 			}
350 		return true;
351 	},
352 	set_orderBy: function(inOrderBy) {
353 		this.checkOrderBy(inOrderBy);
354 		this.setOrderBy(inOrderBy);
355 	},
356 	makePropEdit: function(inName, inValue, inDefault) {
357 		switch (inName) {
358 			case "liveSource":
359 				return new wm.propEdit.LiveSourcesSelect({component: this, name: inName, value: inValue});
360 			case "matchMode":
361 				return makeSelectPropEdit(inName, inValue, this._matchModes, inDefault);
362 			case "operation":
363 				return makeSelectPropEdit(inName, inValue, this._operations, inDefault);
364 		}
365 		return this.inherited(arguments);
366 	}
367 });
368