Projekt

Obecné

Profil

Stáhnout (53.5 KB) Statistiky
| Větev: | Tag: | Revize:
1
<?php
2
// Copyright (c) 2004 ars Cognita Inc., all rights reserved
3
/* ******************************************************************************
4
    Released under both BSD license and Lesser GPL library license. 
5
 	Whenever there is any discrepancy between the two licenses, 
6
 	the BSD license will take precedence. 
7
*******************************************************************************/
8
/**
9
 * xmlschema is a class that allows the user to quickly and easily
10
 * build a database on any ADOdb-supported platform using a simple
11
 * XML schema.
12
 *
13
 * Last Editor: $Author: chriskl $
14
 * @author Richard Tango-Lowy & Dan Cech
15
 * @version $Revision: 1.1 $
16
 *
17
 * @package axmls
18
 * @tutorial getting_started.pkg
19
 */
20

    
21
/**
22
* Debug on or off
23
*/
24
if( !defined( 'XMLS_DEBUG' ) ) {
25
	define( 'XMLS_DEBUG', FALSE );
26
}
27

    
28
/**
29
* Default prefix key
30
*/
31
if( !defined( 'XMLS_PREFIX' ) ) {
32
	define( 'XMLS_PREFIX', '%%P' );
33
}
34

    
35
/**
36
* Maximum length allowed for object prefix
37
*/
38
if( !defined( 'XMLS_PREFIX_MAXLEN' ) ) {
39
	define( 'XMLS_PREFIX_MAXLEN', 10 );
40
}
41

    
42
/**
43
* Execute SQL inline as it is generated
44
*/
45
if( !defined( 'XMLS_EXECUTE_INLINE' ) ) {
46
	define( 'XMLS_EXECUTE_INLINE', FALSE );
47
}
48

    
49
/**
50
* Continue SQL Execution if an error occurs?
51
*/
52
if( !defined( 'XMLS_CONTINUE_ON_ERROR' ) ) {
53
	define( 'XMLS_CONTINUE_ON_ERROR', FALSE );
54
}
55

    
56
/**
57
* Current Schema Version
58
*/
59
if( !defined( 'XMLS_SCHEMA_VERSION' ) ) {
60
	define( 'XMLS_SCHEMA_VERSION', '0.2' );
61
}
62

    
63
/**
64
* Default Schema Version.  Used for Schemas without an explicit version set.
65
*/
66
if( !defined( 'XMLS_DEFAULT_SCHEMA_VERSION' ) ) {
67
	define( 'XMLS_DEFAULT_SCHEMA_VERSION', '0.1' );
68
}
69

    
70
/**
71
* Default Schema Version.  Used for Schemas without an explicit version set.
72
*/
73
if( !defined( 'XMLS_DEFAULT_UPGRADE_METHOD' ) ) {
74
	define( 'XMLS_DEFAULT_UPGRADE_METHOD', 'ALTER' );
75
}
76

    
77
/**
78
* Include the main ADODB library
79
*/
80
if( !defined( '_ADODB_LAYER' ) ) {
81
	require( 'adodb.inc.php' );
82
	require( 'adodb-datadict.inc.php' );
83
}
84

    
85
/**
86
* Abstract DB Object. This class provides basic methods for database objects, such
87
* as tables and indexes.
88
*
89
* @package axmls
90
* @access private
91
*/
92
class dbObject {
93
	
94
	/**
95
	* var object Parent
96
	*/
97
	var $parent;
98
	
99
	/**
100
	* var string current element
101
	*/
102
	var $currentElement;
103
	
104
	/**
105
	* NOP
106
	*/
107
	function dbObject( &$parent, $attributes = NULL ) {
108
		$this->parent =& $parent;
109
	}
110
	
111
	/**
112
	* XML Callback to process start elements
113
	*
114
	* @access private
115
	*/
116
	function _tag_open( &$parser, $tag, $attributes ) {
117
		
118
	}
119
	
120
	/**
121
	* XML Callback to process CDATA elements
122
	*
123
	* @access private
124
	*/
125
	function _tag_cdata( &$parser, $cdata ) {
126
		
127
	}
128
	
129
	/**
130
	* XML Callback to process end elements
131
	*
132
	* @access private
133
	*/
134
	function _tag_close( &$parser, $tag ) {
135
		
136
	}
137
	
138
	function create() {
139
		return array();
140
	}
141
	
142
	/**
143
	* Destroys the object
144
	*/
145
	function destroy() {
146
		unset( $this );
147
	}
148
	
149
	/**
150
	* Checks whether the specified RDBMS is supported by the current
151
	* database object or its ranking ancestor.
152
	*
153
	* @param string $platform RDBMS platform name (from ADODB platform list).
154
	* @return boolean TRUE if RDBMS is supported; otherwise returns FALSE.
155
	*/
156
	function supportedPlatform( $platform = NULL ) {
157
		return is_object( $this->parent ) ? $this->parent->supportedPlatform( $platform ) : TRUE;
158
	}
159
	
160
	/**
161
	* Returns the prefix set by the ranking ancestor of the database object.
162
	*
163
	* @param string $name Prefix string.
164
	* @return string Prefix.
165
	*/
166
	function prefix( $name = '' ) {
167
		return is_object( $this->parent ) ? $this->parent->prefix( $name ) : $name;
168
	}
169
	
170
	/**
171
	* Extracts a field ID from the specified field.
172
	*
173
	* @param string $field Field.
174
	* @return string Field ID.
175
	*/
176
	function FieldID( $field ) {
177
		return strtoupper( preg_replace( '/^`(.+)`$/', '$1', $field ) );
178
	}
179
}
180

    
181
/**
182
* Creates a table object in ADOdb's datadict format
183
*
184
* This class stores information about a database table. As charactaristics
185
* of the table are loaded from the external source, methods and properties
186
* of this class are used to build up the table description in ADOdb's
187
* datadict format.
188
*
189
* @package axmls
190
* @access private
191
*/
192
class dbTable extends dbObject {
193
	
194
	/**
195
	* @var string Table name
196
	*/
197
	var $name;
198
	
199
	/**
200
	* @var array Field specifier: Meta-information about each field
201
	*/
202
	var $fields = array();
203
	
204
	/**
205
	* @var array List of table indexes.
206
	*/
207
	var $indexes = array();
208
	
209
	/**
210
	* @var array Table options: Table-level options
211
	*/
212
	var $opts = array();
213
	
214
	/**
215
	* @var string Field index: Keeps track of which field is currently being processed
216
	*/
217
	var $current_field;
218
	
219
	/**
220
	* @var boolean Mark table for destruction
221
	* @access private
222
	*/
223
	var $drop_table;
224
	
225
	/**
226
	* @var boolean Mark field for destruction (not yet implemented)
227
	* @access private
228
	*/
229
	var $drop_field = array();
230
	
231
	/**
232
	* Iniitializes a new table object.
233
	*
234
	* @param string $prefix DB Object prefix
235
	* @param array $attributes Array of table attributes.
236
	*/
237
	function dbTable( &$parent, $attributes = NULL ) {
238
		$this->parent =& $parent;
239
		$this->name = $this->prefix($attributes['NAME']);
240
	}
241
	
242
	/**
243
	* XML Callback to process start elements. Elements currently 
244
	* processed are: INDEX, DROP, FIELD, KEY, NOTNULL, AUTOINCREMENT & DEFAULT. 
245
	*
246
	* @access private
247
	*/
248
	function _tag_open( &$parser, $tag, $attributes ) {
249
		$this->currentElement = strtoupper( $tag );
250
		
251
		switch( $this->currentElement ) {
252
			case 'INDEX':
253
				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
254
					xml_set_object( $parser, $this->addIndex( $attributes ) );
255
				}
256
				break;
257
			case 'DATA':
258
				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
259
					xml_set_object( $parser, $this->addData( $attributes ) );
260
				}
261
				break;
262
			case 'DROP':
263
				$this->drop();
264
				break;
265
			case 'FIELD':
266
				// Add a field
267
				$fieldName = $attributes['NAME'];
268
				$fieldType = $attributes['TYPE'];
269
				$fieldSize = isset( $attributes['SIZE'] ) ? $attributes['SIZE'] : NULL;
270
				$fieldOpts = isset( $attributes['OPTS'] ) ? $attributes['OPTS'] : NULL;
271
				
272
				$this->addField( $fieldName, $fieldType, $fieldSize, $fieldOpts );
273
				break;
274
			case 'KEY':
275
			case 'NOTNULL':
276
			case 'AUTOINCREMENT':
277
				// Add a field option
278
				$this->addFieldOpt( $this->current_field, $this->currentElement );
279
				break;
280
			case 'DEFAULT':
281
				// Add a field option to the table object
282
				
283
				// Work around ADOdb datadict issue that misinterprets empty strings.
284
				if( $attributes['VALUE'] == '' ) {
285
					$attributes['VALUE'] = " '' ";
286
				}
287
				
288
				$this->addFieldOpt( $this->current_field, $this->currentElement, $attributes['VALUE'] );
289
				break;
290
			case 'DEFDATE':
291
			case 'DEFTIMESTAMP':
292
				// Add a field option to the table object
293
				$this->addFieldOpt( $this->current_field, $this->currentElement );
294
				break;
295
			default:
296
				// print_r( array( $tag, $attributes ) );
297
		}
298
	}
299
	
300
	/**
301
	* XML Callback to process CDATA elements
302
	*
303
	* @access private
304
	*/
305
	function _tag_cdata( &$parser, $cdata ) {
306
		switch( $this->currentElement ) {
307
			// Table constraint
308
			case 'CONSTRAINT':
309
				if( isset( $this->current_field ) ) {
310
					$this->addFieldOpt( $this->current_field, $this->currentElement, $cdata );
311
				} else {
312
					$this->addTableOpt( $cdata );
313
				}
314
				break;
315
			// Table option
316
			case 'OPT':
317
				$this->addTableOpt( $cdata );
318
				break;
319
			default:
320
				
321
		}
322
	}
323
	
324
	/**
325
	* XML Callback to process end elements
326
	*
327
	* @access private
328
	*/
329
	function _tag_close( &$parser, $tag ) {
330
		$this->currentElement = '';
331
		
332
		switch( strtoupper( $tag ) ) {
333
			case 'TABLE':
334
				$this->parent->addSQL( $this->create( $this->parent ) );
335
				xml_set_object( $parser, $this->parent );
336
				$this->destroy();
337
				break;
338
			case 'FIELD':
339
				unset($this->current_field);
340
				break;
341

    
342
		}
343
	}
344
	
345
	/**
346
	* Adds an index to a table object
347
	*
348
	* @param array $attributes Index attributes
349
	* @return object dbIndex object
350
	*/
351
	function &addIndex( $attributes ) {
352
		$name = strtoupper( $attributes['NAME'] );
353
		$this->indexes[$name] =& new dbIndex( $this, $attributes );
354
		return $this->indexes[$name];
355
	}
356
	
357
	/**
358
	* Adds data to a table object
359
	*
360
	* @param array $attributes Data attributes
361
	* @return object dbData object
362
	*/
363
	function &addData( $attributes ) {
364
		if( !isset( $this->data ) ) {
365
			$this->data =& new dbData( $this, $attributes );
366
		}
367
		return $this->data;
368
	}
369
	
370
	/**
371
	* Adds a field to a table object
372
	*
373
	* $name is the name of the table to which the field should be added. 
374
	* $type is an ADODB datadict field type. The following field types
375
	* are supported as of ADODB 3.40:
376
	* 	- C:  varchar
377
	*	- X:  CLOB (character large object) or largest varchar size
378
	*	   if CLOB is not supported
379
	*	- C2: Multibyte varchar
380
	*	- X2: Multibyte CLOB
381
	*	- B:  BLOB (binary large object)
382
	*	- D:  Date (some databases do not support this, and we return a datetime type)
383
	*	- T:  Datetime or Timestamp
384
	*	- L:  Integer field suitable for storing booleans (0 or 1)
385
	*	- I:  Integer (mapped to I4)
386
	*	- I1: 1-byte integer
387
	*	- I2: 2-byte integer
388
	*	- I4: 4-byte integer
389
	*	- I8: 8-byte integer
390
	*	- F:  Floating point number
391
	*	- N:  Numeric or decimal number
392
	*
393
	* @param string $name Name of the table to which the field will be added.
394
	* @param string $type	ADODB datadict field type.
395
	* @param string $size	Field size
396
	* @param array $opts	Field options array
397
	* @return array Field specifier array
398
	*/
399
	function addField( $name, $type, $size = NULL, $opts = NULL ) {
400
		$field_id = $this->FieldID( $name );
401
		
402
		// Set the field index so we know where we are
403
		$this->current_field = $field_id;
404
		
405
		// Set the field name (required)
406
		$this->fields[$field_id]['NAME'] = $name;
407
		
408
		// Set the field type (required)
409
		$this->fields[$field_id]['TYPE'] = $type;
410
		
411
		// Set the field size (optional)
412
		if( isset( $size ) ) {
413
			$this->fields[$field_id]['SIZE'] = $size;
414
		}
415
		
416
		// Set the field options
417
		if( isset( $opts ) ) {
418
			$this->fields[$field_id]['OPTS'][] = $opts;
419
		}
420
	}
421
	
422
	/**
423
	* Adds a field option to the current field specifier
424
	*
425
	* This method adds a field option allowed by the ADOdb datadict 
426
	* and appends it to the given field.
427
	*
428
	* @param string $field	Field name
429
	* @param string $opt ADOdb field option
430
	* @param mixed $value Field option value
431
	* @return array Field specifier array
432
	*/
433
	function addFieldOpt( $field, $opt, $value = NULL ) {
434
		if( !isset( $value ) ) {
435
			$this->fields[$this->FieldID( $field )]['OPTS'][] = $opt;
436
		// Add the option and value
437
		} else {
438
			$this->fields[$this->FieldID( $field )]['OPTS'][] = array( $opt => $value );
439
		}
440
	}
441
	
442
	/**
443
	* Adds an option to the table
444
	*
445
	* This method takes a comma-separated list of table-level options
446
	* and appends them to the table object.
447
	*
448
	* @param string $opt Table option
449
	* @return array Options
450
	*/
451
	function addTableOpt( $opt ) {
452
		$this->opts[] = $opt;
453
		
454
		return $this->opts;
455
	}
456
	
457
	/**
458
	* Generates the SQL that will create the table in the database
459
	*
460
	* @param object $xmls adoSchema object
461
	* @return array Array containing table creation SQL
462
	*/
463
	function create( &$xmls ) {
464
		$sql = array();
465
		
466
		// drop any existing indexes
467
		if( is_array( $legacy_indexes = $xmls->dict->MetaIndexes( $this->name ) ) ) {
468
			foreach( $legacy_indexes as $index => $index_details ) {
469
				$sql[] = $xmls->dict->DropIndexSQL( $index, $this->name );
470
			}
471
		}
472
		
473
		// remove fields to be dropped from table object
474
		foreach( $this->drop_field as $field ) {
475
			unset( $this->fields[$field] );
476
		}
477
		
478
		// if table exists
479
		if( is_array( $legacy_fields = $xmls->dict->MetaColumns( $this->name ) ) ) {
480
			// drop table
481
			if( $this->drop_table ) {
482
				$sql[] = $xmls->dict->DropTableSQL( $this->name );
483
				
484
				return $sql;
485
			}
486
			
487
			// drop any existing fields not in schema
488
			foreach( $legacy_fields as $field_id => $field ) {
489
				if( !isset( $this->fields[$field_id] ) ) {
490
					$sql[] = $xmls->dict->DropColumnSQL( $this->name, '`'.$field->name.'`' );
491
				}
492
			}
493
		// if table doesn't exist
494
		} else {
495
			if( $this->drop_table ) {
496
				return $sql;
497
			}
498
			
499
			$legacy_fields = array();
500
		}
501
		
502
		// Loop through the field specifier array, building the associative array for the field options
503
		$fldarray = array();
504
		
505
		foreach( $this->fields as $field_id => $finfo ) {
506
			// Set an empty size if it isn't supplied
507
			if( !isset( $finfo['SIZE'] ) ) {
508
				$finfo['SIZE'] = '';
509
			}
510
			
511
			// Initialize the field array with the type and size
512
			$fldarray[$field_id] = array(
513
				'NAME' => $finfo['NAME'],
514
				'TYPE' => $finfo['TYPE'],
515
				'SIZE' => $finfo['SIZE']
516
			);
517
			
518
			// Loop through the options array and add the field options. 
519
			if( isset( $finfo['OPTS'] ) ) {
520
				foreach( $finfo['OPTS'] as $opt ) {
521
					// Option has an argument.
522
					if( is_array( $opt ) ) {
523
						$key = key( $opt );
524
						$value = $opt[key( $opt )];
525
						@$fldarray[$field_id][$key] .= $value;
526
					// Option doesn't have arguments
527
					} else {
528
						$fldarray[$field_id][$opt] = $opt;
529
					}
530
				}
531
			}
532
		}
533
		
534
		if( empty( $legacy_fields ) ) {
535
			// Create the new table
536
			$sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts );
537
			logMsg( end( $sql ), 'Generated CreateTableSQL' );
538
		} else {
539
			// Upgrade an existing table
540
			logMsg( "Upgrading {$this->name} using '{$xmls->upgrade}'" );
541
			switch( $xmls->upgrade ) {
542
				// Use ChangeTableSQL
543
				case 'ALTER':
544
					logMsg( 'Generated ChangeTableSQL (ALTERing table)' );
545
					$sql[] = $xmls->dict->ChangeTableSQL( $this->name, $fldarray, $this->opts );
546
					break;
547
				case 'REPLACE':
548
					logMsg( 'Doing upgrade REPLACE (testing)' );
549
					$sql[] = $xmls->dict->DropTableSQL( $this->name );
550
					$sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts );
551
					break;
552
				// ignore table
553
				default:
554
					return array();
555
			}
556
		}
557
		
558
		foreach( $this->indexes as $index ) {
559
			$sql[] = $index->create( $xmls );
560
		}
561
		
562
		if( isset( $this->data ) ) {
563
			$sql[] = $this->data->create( $xmls );
564
		}
565
		
566
		return $sql;
567
	}
568
	
569
	/**
570
	* Marks a field or table for destruction
571
	*/
572
	function drop() {
573
		if( isset( $this->current_field ) ) {
574
			// Drop the current field
575
			logMsg( "Dropping field '{$this->current_field}' from table '{$this->name}'" );
576
			// $this->drop_field[$this->current_field] = $xmls->dict->DropColumnSQL( $this->name, $this->current_field );
577
			$this->drop_field[$this->current_field] = $this->current_field;
578
		} else {
579
			// Drop the current table
580
			logMsg( "Dropping table '{$this->name}'" );
581
			// $this->drop_table = $xmls->dict->DropTableSQL( $this->name );
582
			$this->drop_table = TRUE;
583
		}
584
	}
585
}
586

    
587
/**
588
* Creates an index object in ADOdb's datadict format
589
*
590
* This class stores information about a database index. As charactaristics
591
* of the index are loaded from the external source, methods and properties
592
* of this class are used to build up the index description in ADOdb's
593
* datadict format.
594
*
595
* @package axmls
596
* @access private
597
*/
598
class dbIndex extends dbObject {
599
	
600
	/**
601
	* @var string	Index name
602
	*/
603
	var $name;
604
	
605
	/**
606
	* @var array	Index options: Index-level options
607
	*/
608
	var $opts = array();
609
	
610
	/**
611
	* @var array	Indexed fields: Table columns included in this index
612
	*/
613
	var $columns = array();
614
	
615
	/**
616
	* @var boolean Mark index for destruction
617
	* @access private
618
	*/
619
	var $drop = FALSE;
620
	
621
	/**
622
	* Initializes the new dbIndex object.
623
	*
624
	* @param object $parent Parent object
625
	* @param array $attributes Attributes
626
	*
627
	* @internal
628
	*/
629
	function dbIndex( &$parent, $attributes = NULL ) {
630
		$this->parent =& $parent;
631
		
632
		$this->name = $this->prefix ($attributes['NAME']);
633
	}
634
	
635
	/**
636
	* XML Callback to process start elements
637
	*
638
	* Processes XML opening tags. 
639
	* Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH. 
640
	*
641
	* @access private
642
	*/
643
	function _tag_open( &$parser, $tag, $attributes ) {
644
		$this->currentElement = strtoupper( $tag );
645
		
646
		switch( $this->currentElement ) {
647
			case 'DROP':
648
				$this->drop();
649
				break;
650
			case 'CLUSTERED':
651
			case 'BITMAP':
652
			case 'UNIQUE':
653
			case 'FULLTEXT':
654
			case 'HASH':
655
				// Add index Option
656
				$this->addIndexOpt( $this->currentElement );
657
				break;
658
			default:
659
				// print_r( array( $tag, $attributes ) );
660
		}
661
	}
662
	
663
	/**
664
	* XML Callback to process CDATA elements
665
	*
666
	* Processes XML cdata.
667
	*
668
	* @access private
669
	*/
670
	function _tag_cdata( &$parser, $cdata ) {
671
		switch( $this->currentElement ) {
672
			// Index field name
673
			case 'COL':
674
				$this->addField( $cdata );
675
				break;
676
			default:
677
				
678
		}
679
	}
680
	
681
	/**
682
	* XML Callback to process end elements
683
	*
684
	* @access private
685
	*/
686
	function _tag_close( &$parser, $tag ) {
687
		$this->currentElement = '';
688
		
689
		switch( strtoupper( $tag ) ) {
690
			case 'INDEX':
691
				xml_set_object( $parser, $this->parent );
692
				break;
693
		}
694
	}
695
	
696
	/**
697
	* Adds a field to the index
698
	*
699
	* @param string $name Field name
700
	* @return string Field list
701
	*/
702
	function addField( $name ) {
703
		$this->columns[$this->FieldID( $name )] = $name;
704
		
705
		// Return the field list
706
		return $this->columns;
707
	}
708
	
709
	/**
710
	* Adds options to the index
711
	*
712
	* @param string $opt Comma-separated list of index options.
713
	* @return string Option list
714
	*/
715
	function addIndexOpt( $opt ) {
716
		$this->opts[] = $opt;
717
		
718
		// Return the options list
719
		return $this->opts;
720
	}
721
	
722
	/**
723
	* Generates the SQL that will create the index in the database
724
	*
725
	* @param object $xmls adoSchema object
726
	* @return array Array containing index creation SQL
727
	*/
728
	function create( &$xmls ) {
729
		if( $this->drop ) {
730
			return NULL;
731
		}
732
		
733
		// eliminate any columns that aren't in the table
734
		foreach( $this->columns as $id => $col ) {
735
			if( !isset( $this->parent->fields[$id] ) ) {
736
				unset( $this->columns[$id] );
737
			}
738
		}
739
		
740
		return $xmls->dict->CreateIndexSQL( $this->name, $this->parent->name, $this->columns, $this->opts );
741
	}
742
	
743
	/**
744
	* Marks an index for destruction
745
	*/
746
	function drop() {
747
		$this->drop = TRUE;
748
	}
749
}
750

    
751
/**
752
* Creates a data object in ADOdb's datadict format
753
*
754
* This class stores information about table data.
755
*
756
* @package axmls
757
* @access private
758
*/
759
class dbData extends dbObject {
760
	
761
	var $data = array();
762
	
763
	var $row;
764
	
765
	/**
766
	* Initializes the new dbIndex object.
767
	*
768
	* @param object $parent Parent object
769
	* @param array $attributes Attributes
770
	*
771
	* @internal
772
	*/
773
	function dbData( &$parent, $attributes = NULL ) {
774
		$this->parent =& $parent;
775
	}
776
	
777
	/**
778
	* XML Callback to process start elements
779
	*
780
	* Processes XML opening tags. 
781
	* Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH. 
782
	*
783
	* @access private
784
	*/
785
	function _tag_open( &$parser, $tag, $attributes ) {
786
		$this->currentElement = strtoupper( $tag );
787
		
788
		switch( $this->currentElement ) {
789
			case 'ROW':
790
				$this->row = count( $this->data );
791
				$this->data[$this->row] = array();
792
				break;
793
			case 'F':
794
				$this->addField($attributes);
795
			default:
796
				// print_r( array( $tag, $attributes ) );
797
		}
798
	}
799
	
800
	/**
801
	* XML Callback to process CDATA elements
802
	*
803
	* Processes XML cdata.
804
	*
805
	* @access private
806
	*/
807
	function _tag_cdata( &$parser, $cdata ) {
808
		switch( $this->currentElement ) {
809
			// Index field name
810
			case 'F':
811
				$this->addData( $cdata );
812
				break;
813
			default:
814
				
815
		}
816
	}
817
	
818
	/**
819
	* XML Callback to process end elements
820
	*
821
	* @access private
822
	*/
823
	function _tag_close( &$parser, $tag ) {
824
		$this->currentElement = '';
825
		
826
		switch( strtoupper( $tag ) ) {
827
			case 'DATA':
828
				xml_set_object( $parser, $this->parent );
829
				break;
830
		}
831
	}
832
	
833
	/**
834
	* Adds a field to the index
835
	*
836
	* @param string $name Field name
837
	* @return string Field list
838
	*/
839
	function addField( $attributes ) {
840
		if( isset( $attributes['NAME'] ) ) {
841
			$name = $attributes['NAME'];
842
		} else {
843
			$name = count($this->data[$this->row]);
844
		}
845
		
846
		// Set the field index so we know where we are
847
		$this->current_field = $this->FieldID( $name );
848
	}
849
	
850
	/**
851
	* Adds options to the index
852
	*
853
	* @param string $opt Comma-separated list of index options.
854
	* @return string Option list
855
	*/
856
	function addData( $cdata ) {
857
		if( !isset( $this->data[$this->row] ) ) {
858
			$this->data[$this->row] = array();
859
		}
860
		
861
		if( !isset( $this->data[$this->row][$this->current_field] ) ) {
862
			$this->data[$this->row][$this->current_field] = '';
863
		}
864
		
865
		$this->data[$this->row][$this->current_field] .= $cdata;
866
	}
867
	
868
	/**
869
	* Generates the SQL that will create the index in the database
870
	*
871
	* @param object $xmls adoSchema object
872
	* @return array Array containing index creation SQL
873
	*/
874
	function create( &$xmls ) {
875
		$table = $xmls->dict->TableName($this->parent->name);
876
		$table_field_count = count($this->parent->fields);
877
		$sql = array();
878
		
879
		// eliminate any columns that aren't in the table
880
		foreach( $this->data as $row ) {
881
			$table_fields = $this->parent->fields;
882
			$fields = array();
883
			
884
			foreach( $row as $field_id => $field_data ) {
885
				if( !array_key_exists( $field_id, $table_fields ) ) {
886
					if( is_numeric( $field_id ) ) {
887
						$field_id = reset( array_keys( $table_fields ) );
888
					} else {
889
						continue;
890
					}
891
				}
892
				
893
				$name = $table_fields[$field_id]['NAME'];
894
				
895
				switch( $table_fields[$field_id]['TYPE'] ) {
896
					case 'C':
897
					case 'C2':
898
					case 'X':
899
					case 'X2':
900
						$fields[$name] = $xmls->db->qstr( $field_data );
901
						break;
902
					case 'I':
903
					case 'I1':
904
					case 'I2':
905
					case 'I4':
906
					case 'I8':
907
						$fields[$name] = intval($field_data);
908
						break;
909
					default:
910
						$fields[$name] = $field_data;
911
				}
912
				
913
				unset($table_fields[$field_id]);
914
			}
915
			
916
			// check that at least 1 column is specified
917
			if( empty( $fields ) ) {
918
				continue;
919
			}
920
			
921
			// check that no required columns are missing
922
			if( count( $fields ) < $table_field_count ) {
923
				foreach( $table_fields as $field ) {
924
					if (isset( $field['OPTS'] ))
925
						if( ( in_array( 'NOTNULL', $field['OPTS'] ) || in_array( 'KEY', $field['OPTS'] ) ) && !in_array( 'AUTOINCREMENT', $field['OPTS'] ) ) {
926
							continue(2);
927
						}
928
				}
929
			}
930
			
931
			$sql[] = 'INSERT INTO '. $table .' ('. implode( ',', array_keys( $fields ) ) .') VALUES ('. implode( ',', $fields ) .')';
932
		}
933
		
934
		return $sql;
935
	}
936
}
937

    
938
/**
939
* Creates the SQL to execute a list of provided SQL queries
940
*
941
* @package axmls
942
* @access private
943
*/
944
class dbQuerySet extends dbObject {
945
	
946
	/**
947
	* @var array	List of SQL queries
948
	*/
949
	var $queries = array();
950
	
951
	/**
952
	* @var string	String used to build of a query line by line
953
	*/
954
	var $query;
955
	
956
	/**
957
	* @var string	Query prefix key
958
	*/
959
	var $prefixKey = '';
960
	
961
	/**
962
	* @var boolean	Auto prefix enable (TRUE)
963
	*/
964
	var $prefixMethod = 'AUTO';
965
	
966
	/**
967
	* Initializes the query set.
968
	*
969
	* @param object $parent Parent object
970
	* @param array $attributes Attributes
971
	*/
972
	function dbQuerySet( &$parent, $attributes = NULL ) {
973
		$this->parent =& $parent;
974
			
975
		// Overrides the manual prefix key
976
		if( isset( $attributes['KEY'] ) ) {
977
			$this->prefixKey = $attributes['KEY'];
978
		}
979
		
980
		$prefixMethod = isset( $attributes['PREFIXMETHOD'] ) ? strtoupper( trim( $attributes['PREFIXMETHOD'] ) ) : '';
981
		
982
		// Enables or disables automatic prefix prepending
983
		switch( $prefixMethod ) {
984
			case 'AUTO':
985
				$this->prefixMethod = 'AUTO';
986
				break;
987
			case 'MANUAL':
988
				$this->prefixMethod = 'MANUAL';
989
				break;
990
			case 'NONE':
991
				$this->prefixMethod = 'NONE';
992
				break;
993
		}
994
	}
995
	
996
	/**
997
	* XML Callback to process start elements. Elements currently 
998
	* processed are: QUERY. 
999
	*
1000
	* @access private
1001
	*/
1002
	function _tag_open( &$parser, $tag, $attributes ) {
1003
		$this->currentElement = strtoupper( $tag );
1004
		
1005
		switch( $this->currentElement ) {
1006
			case 'QUERY':
1007
				// Create a new query in a SQL queryset.
1008
				// Ignore this query set if a platform is specified and it's different than the 
1009
				// current connection platform.
1010
				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
1011
					$this->newQuery();
1012
				} else {
1013
					$this->discardQuery();
1014
				}
1015
				break;
1016
			default:
1017
				// print_r( array( $tag, $attributes ) );
1018
		}
1019
	}
1020
	
1021
	/**
1022
	* XML Callback to process CDATA elements
1023
	*/
1024
	function _tag_cdata( &$parser, $cdata ) {
1025
		switch( $this->currentElement ) {
1026
			// Line of queryset SQL data
1027
			case 'QUERY':
1028
				$this->buildQuery( $cdata );
1029
				break;
1030
			default:
1031
				
1032
		}
1033
	}
1034
	
1035
	/**
1036
	* XML Callback to process end elements
1037
	*
1038
	* @access private
1039
	*/
1040
	function _tag_close( &$parser, $tag ) {
1041
		$this->currentElement = '';
1042
		
1043
		switch( strtoupper( $tag ) ) {
1044
			case 'QUERY':
1045
				// Add the finished query to the open query set.
1046
				$this->addQuery();
1047
				break;
1048
			case 'SQL':
1049
				$this->parent->addSQL( $this->create( $this->parent ) );
1050
				xml_set_object( $parser, $this->parent );
1051
				$this->destroy();
1052
				break;
1053
			default:
1054
				
1055
		}
1056
	}
1057
	
1058
	/**
1059
	* Re-initializes the query.
1060
	*
1061
	* @return boolean TRUE
1062
	*/
1063
	function newQuery() {
1064
		$this->query = '';
1065
		
1066
		return TRUE;
1067
	}
1068
	
1069
	/**
1070
	* Discards the existing query.
1071
	*
1072
	* @return boolean TRUE
1073
	*/
1074
	function discardQuery() {
1075
		unset( $this->query );
1076
		
1077
		return TRUE;
1078
	}
1079
	
1080
	/** 
1081
	* Appends a line to a query that is being built line by line
1082
	*
1083
	* @param string $data Line of SQL data or NULL to initialize a new query
1084
	* @return string SQL query string.
1085
	*/
1086
	function buildQuery( $sql = NULL ) {
1087
		if( !isset( $this->query ) OR empty( $sql ) ) {
1088
			return FALSE;
1089
		}
1090
		
1091
		$this->query .= $sql;
1092
		
1093
		return $this->query;
1094
	}
1095
	
1096
	/**
1097
	* Adds a completed query to the query list
1098
	*
1099
	* @return string	SQL of added query
1100
	*/
1101
	function addQuery() {
1102
		if( !isset( $this->query ) ) {
1103
			return FALSE;
1104
		}
1105
		
1106
		$this->queries[] = $return = trim($this->query);
1107
		
1108
		unset( $this->query );
1109
		
1110
		return $return;
1111
	}
1112
	
1113
	/**
1114
	* Creates and returns the current query set
1115
	*
1116
	* @param object $xmls adoSchema object
1117
	* @return array Query set
1118
	*/
1119
	function create( &$xmls ) {
1120
		foreach( $this->queries as $id => $query ) {
1121
			switch( $this->prefixMethod ) {
1122
				case 'AUTO':
1123
					// Enable auto prefix replacement
1124
					
1125
					// Process object prefix.
1126
					// Evaluate SQL statements to prepend prefix to objects
1127
					$query = $this->prefixQuery( '/^\s*((?is)INSERT\s+(INTO\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1128
					$query = $this->prefixQuery( '/^\s*((?is)UPDATE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1129
					$query = $this->prefixQuery( '/^\s*((?is)DELETE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1130
					
1131
					// SELECT statements aren't working yet
1132
					#$data = preg_replace( '/(?ias)(^\s*SELECT\s+.*\s+FROM)\s+(\W\s*,?\s*)+((?i)\s+WHERE.*$)/', "\1 $prefix\2 \3", $data );
1133
					
1134
				case 'MANUAL':
1135
					// If prefixKey is set and has a value then we use it to override the default constant XMLS_PREFIX.
1136
					// If prefixKey is not set, we use the default constant XMLS_PREFIX
1137
					if( isset( $this->prefixKey ) AND( $this->prefixKey !== '' ) ) {
1138
						// Enable prefix override
1139
						$query = str_replace( $this->prefixKey, $xmls->objectPrefix, $query );
1140
					} else {
1141
						// Use default replacement
1142
						$query = str_replace( XMLS_PREFIX , $xmls->objectPrefix, $query );
1143
					}
1144
			}
1145
			
1146
			$this->queries[$id] = trim( $query );
1147
		}
1148
		
1149
		// Return the query set array
1150
		return $this->queries;
1151
	}
1152
	
1153
	/**
1154
	* Rebuilds the query with the prefix attached to any objects
1155
	*
1156
	* @param string $regex Regex used to add prefix
1157
	* @param string $query SQL query string
1158
	* @param string $prefix Prefix to be appended to tables, indices, etc.
1159
	* @return string Prefixed SQL query string.
1160
	*/
1161
	function prefixQuery( $regex, $query, $prefix = NULL ) {
1162
		if( !isset( $prefix ) ) {
1163
			return $query;
1164
		}
1165
		
1166
		if( preg_match( $regex, $query, $match ) ) {
1167
			$preamble = $match[1];
1168
			$postamble = $match[5];
1169
			$objectList = explode( ',', $match[3] );
1170
			// $prefix = $prefix . '_';
1171
			
1172
			$prefixedList = '';
1173
			
1174
			foreach( $objectList as $object ) {
1175
				if( $prefixedList !== '' ) {
1176
					$prefixedList .= ', ';
1177
				}
1178
				
1179
				$prefixedList .= $prefix . trim( $object );
1180
			}
1181
			
1182
			$query = $preamble . ' ' . $prefixedList . ' ' . $postamble;
1183
		}
1184
		
1185
		return $query;
1186
	}
1187
}
1188

    
1189
/**
1190
* Loads and parses an XML file, creating an array of "ready-to-run" SQL statements
1191
* 
1192
* This class is used to load and parse the XML file, to create an array of SQL statements
1193
* that can be used to build a database, and to build the database using the SQL array.
1194
*
1195
* @tutorial getting_started.pkg
1196
*
1197
* @author Richard Tango-Lowy & Dan Cech
1198
* @version $Revision: 1.1 $
1199
*
1200
* @package axmls
1201
*/
1202
class adoSchema {
1203
	
1204
	/**
1205
	* @var array	Array containing SQL queries to generate all objects
1206
	* @access private
1207
	*/
1208
	var $sqlArray;
1209
	
1210
	/**
1211
	* @var object	ADOdb connection object
1212
	* @access private
1213
	*/
1214
	var $db;
1215
	
1216
	/**
1217
	* @var object	ADOdb Data Dictionary
1218
	* @access private
1219
	*/
1220
	var $dict;
1221
	
1222
	/**
1223
	* @var string Current XML element
1224
	* @access private
1225
	*/
1226
	var $currentElement = '';
1227
	
1228
	/**
1229
	* @var string If set (to 'ALTER' or 'REPLACE'), upgrade an existing database
1230
	* @access private
1231
	*/
1232
	var $upgrade = '';
1233
	
1234
	/**
1235
	* @var string Optional object prefix
1236
	* @access private
1237
	*/
1238
	var $objectPrefix = '';
1239
	
1240
	/**
1241
	* @var long	Original Magic Quotes Runtime value
1242
	* @access private
1243
	*/
1244
	var $mgq;
1245
	
1246
	/**
1247
	* @var long	System debug
1248
	* @access private
1249
	*/
1250
	var $debug;
1251
	
1252
	/**
1253
	* @var string Regular expression to find schema version
1254
	* @access private
1255
	*/
1256
	var $versionRegex = '/<schema.*?( version="([^"]*)")?.*?>/';
1257
	
1258
	/**
1259
	* @var string Current schema version
1260
	* @access private
1261
	*/
1262
	var $schemaVersion;
1263
	
1264
	/**
1265
	* @var int	Success of last Schema execution
1266
	*/
1267
	var $success;
1268
	
1269
	/**
1270
	* @var bool	Execute SQL inline as it is generated
1271
	*/
1272
	var $executeInline;
1273
	
1274
	/**
1275
	* @var bool	Continue SQL execution if errors occur
1276
	*/
1277
	var $continueOnError;
1278
	
1279
	/**
1280
	* Creates an adoSchema object
1281
	*
1282
	* Creating an adoSchema object is the first step in processing an XML schema.
1283
	* The only parameter is an ADOdb database connection object, which must already
1284
	* have been created.
1285
	*
1286
	* @param object $db ADOdb database connection object.
1287
	*/
1288
	function adoSchema( &$db ) {
1289
		// Initialize the environment
1290
		$this->mgq = get_magic_quotes_runtime();
1291
		set_magic_quotes_runtime(0);
1292
		
1293
		$this->db =& $db;
1294
		$this->debug = $this->db->debug;
1295
		$this->dict = NewDataDictionary( $this->db );
1296
		$this->sqlArray = array();
1297
		$this->schemaVersion = XMLS_SCHEMA_VERSION;
1298
		$this->executeInline( XMLS_EXECUTE_INLINE );
1299
		$this->continueOnError( XMLS_CONTINUE_ON_ERROR );
1300
		$this->setUpgradeMethod();
1301
	}
1302
	
1303
	/**
1304
	* Sets the method to be used for upgrading an existing database
1305
	*
1306
	* Use this method to specify how existing database objects should be upgraded.
1307
	* The method option can be set to ALTER, REPLACE, BEST, or NONE. ALTER attempts to
1308
	* alter each database object directly, REPLACE attempts to rebuild each object
1309
	* from scratch, BEST attempts to determine the best upgrade method for each
1310
	* object, and NONE disables upgrading.
1311
	*
1312
	* This method is not yet used by AXMLS, but exists for backward compatibility.
1313
	* The ALTER method is automatically assumed when the adoSchema object is
1314
	* instantiated; other upgrade methods are not currently supported.
1315
	*
1316
	* @param string $method Upgrade method (ALTER|REPLACE|BEST|NONE)
1317
	* @returns string Upgrade method used
1318
	*/
1319
	function SetUpgradeMethod( $method = '' ) {
1320
		if( !is_string( $method ) ) {
1321
			return FALSE;
1322
		}
1323
		
1324
		$method = strtoupper( $method );
1325
		
1326
		// Handle the upgrade methods
1327
		switch( $method ) {
1328
			case 'ALTER':
1329
				$this->upgrade = $method;
1330
				break;
1331
			case 'REPLACE':
1332
				$this->upgrade = $method;
1333
				break;
1334
			case 'BEST':
1335
				$this->upgrade = 'ALTER';
1336
				break;
1337
			case 'NONE':
1338
				$this->upgrade = 'NONE';
1339
				break;
1340
			default:
1341
				// Use default if no legitimate method is passed.
1342
				$this->upgrade = XMLS_DEFAULT_UPGRADE_METHOD;
1343
		}
1344
		
1345
		return $this->upgrade;
1346
	}
1347
	
1348
	/**
1349
	* Enables/disables inline SQL execution.
1350
	*
1351
	* Call this method to enable or disable inline execution of the schema. If the mode is set to TRUE (inline execution),
1352
	* AXMLS applies the SQL to the database immediately as each schema entity is parsed. If the mode
1353
	* is set to FALSE (post execution), AXMLS parses the entire schema and you will need to call adoSchema::ExecuteSchema()
1354
	* to apply the schema to the database.
1355
	*
1356
	* @param bool $mode execute
1357
	* @return bool current execution mode
1358
	*
1359
	* @see ParseSchema(), ExecuteSchema()
1360
	*/
1361
	function ExecuteInline( $mode = NULL ) {
1362
		if( is_bool( $mode ) ) {
1363
			$this->executeInline = $mode;
1364
		}
1365
		
1366
		return $this->executeInline;
1367
	}
1368
	
1369
	/**
1370
	* Enables/disables SQL continue on error.
1371
	*
1372
	* Call this method to enable or disable continuation of SQL execution if an error occurs.
1373
	* If the mode is set to TRUE (continue), AXMLS will continue to apply SQL to the database, even if an error occurs.
1374
	* If the mode is set to FALSE (halt), AXMLS will halt execution of generated sql if an error occurs, though parsing
1375
	* of the schema will continue.
1376
	*
1377
	* @param bool $mode execute
1378
	* @return bool current continueOnError mode
1379
	*
1380
	* @see addSQL(), ExecuteSchema()
1381
	*/
1382
	function ContinueOnError( $mode = NULL ) {
1383
		if( is_bool( $mode ) ) {
1384
			$this->continueOnError = $mode;
1385
		}
1386
		
1387
		return $this->continueOnError;
1388
	}
1389
	
1390
	/**
1391
	* Loads an XML schema from a file and converts it to SQL.
1392
	*
1393
	* Call this method to load the specified schema (see the DTD for the proper format) from
1394
	* the filesystem and generate the SQL necessary to create the database described. 
1395
	* @see ParseSchemaString()
1396
	*
1397
	* @param string $file Name of XML schema file.
1398
	* @param bool $returnSchema Return schema rather than parsing.
1399
	* @return array Array of SQL queries, ready to execute
1400
	*/
1401
	function ParseSchema( $filename, $returnSchema = FALSE ) {
1402
		return $this->ParseSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema );
1403
	}
1404
	
1405
	/**
1406
	* Loads an XML schema from a file and converts it to SQL.
1407
	*
1408
	* Call this method to load the specified schema from a file (see the DTD for the proper format) 
1409
	* and generate the SQL necessary to create the database described by the schema.
1410
	*
1411
	* @param string $file Name of XML schema file.
1412
	* @param bool $returnSchema Return schema rather than parsing.
1413
	* @return array Array of SQL queries, ready to execute.
1414
	*
1415
	* @deprecated Replaced by adoSchema::ParseSchema() and adoSchema::ParseSchemaString()
1416
	* @see ParseSchema(), ParseSchemaString()
1417
	*/
1418
	function ParseSchemaFile( $filename, $returnSchema = FALSE ) {
1419
		// Open the file
1420
		if( !($fp = fopen( $filename, 'r' )) ) {
1421
			// die( 'Unable to open file' );
1422
			return FALSE;
1423
		}
1424
		
1425
		// do version detection here
1426
		if( $this->SchemaFileVersion( $filename ) != $this->schemaVersion ) {
1427
			return FALSE;
1428
		}
1429
		
1430
		if ( $returnSchema )
1431
		{
1432
			return $xmlstring;
1433
		}
1434
		
1435
		$this->success = 2;
1436
		
1437
		$xmlParser = $this->create_parser();
1438
		
1439
		// Process the file
1440
		while( $data = fread( $fp, 4096 ) ) {
1441
			if( !xml_parse( $xmlParser, $data, feof( $fp ) ) ) {
1442
				die( sprintf(
1443
					"XML error: %s at line %d",
1444
					xml_error_string( xml_get_error_code( $xmlParser) ),
1445
					xml_get_current_line_number( $xmlParser)
1446
				) );
1447
			}
1448
		}
1449
		
1450
		xml_parser_free( $xmlParser );
1451
		
1452
		return $this->sqlArray;
1453
	}
1454
	
1455
	/**
1456
	* Converts an XML schema string to SQL.
1457
	*
1458
	* Call this method to parse a string containing an XML schema (see the DTD for the proper format)
1459
	* and generate the SQL necessary to create the database described by the schema. 
1460
	* @see ParseSchema()
1461
	*
1462
	* @param string $xmlstring XML schema string.
1463
	* @param bool $returnSchema Return schema rather than parsing.
1464
	* @return array Array of SQL queries, ready to execute.
1465
	*/
1466
	function ParseSchemaString( $xmlstring, $returnSchema = FALSE ) {
1467
		if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) {
1468
			return FALSE;
1469
		}
1470
		
1471
		// do version detection here
1472
		if( $this->SchemaStringVersion( $xmlstring ) != $this->schemaVersion ) {
1473
			return FALSE;
1474
		}
1475
		
1476
		if ( $returnSchema )
1477
		{
1478
			return $xmlstring;
1479
		}
1480
		
1481
		$this->success = 2;
1482
		
1483
		$xmlParser = $this->create_parser();
1484
		
1485
		if( !xml_parse( $xmlParser, $xmlstring, TRUE ) ) {
1486
			die( sprintf(
1487
				"XML error: %s at line %d",
1488
				xml_error_string( xml_get_error_code( $xmlParser) ),
1489
				xml_get_current_line_number( $xmlParser)
1490
			) );
1491
		}
1492
		
1493
		xml_parser_free( $xmlParser );
1494
		
1495
		return $this->sqlArray;
1496
	}
1497
	
1498
	/**
1499
	* Loads an XML schema from a file and converts it to uninstallation SQL.
1500
	*
1501
	* Call this method to load the specified schema (see the DTD for the proper format) from
1502
	* the filesystem and generate the SQL necessary to remove the database described.
1503
	* @see RemoveSchemaString()
1504
	*
1505
	* @param string $file Name of XML schema file.
1506
	* @param bool $returnSchema Return schema rather than parsing.
1507
	* @return array Array of SQL queries, ready to execute
1508
	*/
1509
	function RemoveSchema( $filename, $returnSchema = FALSE ) {
1510
		return $this->RemoveSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema );
1511
	}
1512
	
1513
	/**
1514
	* Converts an XML schema string to uninstallation SQL.
1515
	*
1516
	* Call this method to parse a string containing an XML schema (see the DTD for the proper format)
1517
	* and generate the SQL necessary to uninstall the database described by the schema. 
1518
	* @see RemoveSchema()
1519
	*
1520
	* @param string $schema XML schema string.
1521
	* @param bool $returnSchema Return schema rather than parsing.
1522
	* @return array Array of SQL queries, ready to execute.
1523
	*/
1524
	function RemoveSchemaString( $schema, $returnSchema = FALSE ) {
1525
		
1526
		// grab current version
1527
		if( !( $version = $this->SchemaStringVersion( $schema ) ) ) {
1528
			return FALSE;
1529
		}
1530
		
1531
		return $this->ParseSchemaString( $this->TransformSchema( $schema, 'remove-' . $version), $returnSchema );
1532
	}
1533
	
1534
	/**
1535
	* Applies the current XML schema to the database (post execution).
1536
	*
1537
	* Call this method to apply the current schema (generally created by calling 
1538
	* ParseSchema() or ParseSchemaString() ) to the database (creating the tables, indexes, 
1539
	* and executing other SQL specified in the schema) after parsing.
1540
	* @see ParseSchema(), ParseSchemaString(), ExecuteInline()
1541
	*
1542
	* @param array $sqlArray Array of SQL statements that will be applied rather than
1543
	*		the current schema.
1544
	* @param boolean $continueOnErr Continue to apply the schema even if an error occurs.
1545
	* @returns integer 0 if failure, 1 if errors, 2 if successful.
1546
	*/
1547
	function ExecuteSchema( $sqlArray = NULL, $continueOnErr =  NULL ) {
1548
		if( !is_bool( $continueOnErr ) ) {
1549
			$continueOnErr = $this->ContinueOnError();
1550
		}
1551
		
1552
		if( !isset( $sqlArray ) ) {
1553
			$sqlArray = $this->sqlArray;
1554
		}
1555
		
1556
		if( !is_array( $sqlArray ) ) {
1557
			$this->success = 0;
1558
		} else {
1559
			$this->success = $this->dict->ExecuteSQLArray( $sqlArray, $continueOnErr );
1560
		}
1561
		
1562
		return $this->success;
1563
	}
1564
	
1565
	/**
1566
	* Returns the current SQL array. 
1567
	*
1568
	* Call this method to fetch the array of SQL queries resulting from 
1569
	* ParseSchema() or ParseSchemaString(). 
1570
	*
1571
	* @param string $format Format: HTML, TEXT, or NONE (PHP array)
1572
	* @return array Array of SQL statements or FALSE if an error occurs
1573
	*/
1574
	function PrintSQL( $format = 'NONE' ) {
1575
		return $this->getSQL( $format, $sqlArray );
1576
	}
1577
	
1578
	/**
1579
	* Saves the current SQL array to the local filesystem as a list of SQL queries.
1580
	*
1581
	* Call this method to save the array of SQL queries (generally resulting from a
1582
	* parsed XML schema) to the filesystem.
1583
	*
1584
	* @param string $filename Path and name where the file should be saved.
1585
	* @return boolean TRUE if save is successful, else FALSE. 
1586
	*/
1587
	function SaveSQL( $filename = './schema.sql' ) {
1588
		
1589
		if( !isset( $sqlArray ) ) {
1590
			$sqlArray = $this->sqlArray;
1591
		}
1592
		if( !isset( $sqlArray ) ) {
1593
			return FALSE;
1594
		}
1595
		
1596
		$fp = fopen( $filename, "w" );
1597
		
1598
		foreach( $sqlArray as $key => $query ) {
1599
			fwrite( $fp, $query . ";\n" );
1600
		}
1601
		fclose( $fp );
1602
	}
1603
	
1604
	/**
1605
	* Create an xml parser
1606
	*
1607
	* @return object PHP XML parser object
1608
	*
1609
	* @access private
1610
	*/
1611
	function &create_parser() {
1612
		// Create the parser
1613
		$xmlParser = xml_parser_create();
1614
		xml_set_object( $xmlParser, $this );
1615
		
1616
		// Initialize the XML callback functions
1617
		xml_set_element_handler( $xmlParser, '_tag_open', '_tag_close' );
1618
		xml_set_character_data_handler( $xmlParser, '_tag_cdata' );
1619
		
1620
		return $xmlParser;
1621
	}
1622
	
1623
	/**
1624
	* XML Callback to process start elements
1625
	*
1626
	* @access private
1627
	*/
1628
	function _tag_open( &$parser, $tag, $attributes ) {
1629
		switch( strtoupper( $tag ) ) {
1630
			case 'TABLE':
1631
				$this->obj = new dbTable( $this, $attributes );
1632
				xml_set_object( $parser, $this->obj );
1633
				break;
1634
			case 'SQL':
1635
				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
1636
					$this->obj = new dbQuerySet( $this, $attributes );
1637
					xml_set_object( $parser, $this->obj );
1638
				}
1639
				break;
1640
			default:
1641
				// print_r( array( $tag, $attributes ) );
1642
		}
1643
		
1644
	}
1645
	
1646
	/**
1647
	* XML Callback to process CDATA elements
1648
	*
1649
	* @access private
1650
	*/
1651
	function _tag_cdata( &$parser, $cdata ) {
1652
	}
1653
	
1654
	/**
1655
	* XML Callback to process end elements
1656
	*
1657
	* @access private
1658
	* @internal
1659
	*/
1660
	function _tag_close( &$parser, $tag ) {
1661
		
1662
	}
1663
	
1664
	/**
1665
	* Converts an XML schema string to the specified DTD version.
1666
	*
1667
	* Call this method to convert a string containing an XML schema to a different AXMLS
1668
	* DTD version. For instance, to convert a schema created for an pre-1.0 version for 
1669
	* AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version 
1670
	* parameter is specified, the schema will be converted to the current DTD version. 
1671
	* If the newFile parameter is provided, the converted schema will be written to the specified
1672
	* file.
1673
	* @see ConvertSchemaFile()
1674
	*
1675
	* @param string $schema String containing XML schema that will be converted.
1676
	* @param string $newVersion DTD version to convert to.
1677
	* @param string $newFile File name of (converted) output file.
1678
	* @return string Converted XML schema or FALSE if an error occurs.
1679
	*/
1680
	function ConvertSchemaString( $schema, $newVersion = NULL, $newFile = NULL ) {
1681
		
1682
		// grab current version
1683
		if( !( $version = $this->SchemaStringVersion( $schema ) ) ) {
1684
			return FALSE;
1685
		}
1686
		
1687
		if( !isset ($newVersion) ) {
1688
			$newVersion = $this->schemaVersion;
1689
		}
1690
		
1691
		if( $version == $newVersion ) {
1692
			$result = $schema;
1693
		} else {
1694
			$result = $this->TransformSchema( $schema, 'convert-' . $version . '-' . $newVersion);
1695
		}
1696
		
1697
		if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) {
1698
			fwrite( $fp, $result );
1699
			fclose( $fp );
1700
		}
1701
		
1702
		return $result;
1703
	}
1704
	
1705
	/**
1706
	* Converts an XML schema file to the specified DTD version.
1707
	*
1708
	* Call this method to convert the specified XML schema file to a different AXMLS
1709
	* DTD version. For instance, to convert a schema created for an pre-1.0 version for 
1710
	* AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version 
1711
	* parameter is specified, the schema will be converted to the current DTD version. 
1712
	* If the newFile parameter is provided, the converted schema will be written to the specified
1713
	* file.
1714
	* @see ConvertSchemaString()
1715
	*
1716
	* @param string $filename Name of XML schema file that will be converted.
1717
	* @param string $newVersion DTD version to convert to.
1718
	* @param string $newFile File name of (converted) output file.
1719
	* @return string Converted XML schema or FALSE if an error occurs.
1720
	*/
1721
	function ConvertSchemaFile( $filename, $newVersion = NULL, $newFile = NULL ) {
1722
		
1723
		// grab current version
1724
		if( !( $version = $this->SchemaFileVersion( $filename ) ) ) {
1725
			return FALSE;
1726
		}
1727
		
1728
		if( !isset ($newVersion) ) {
1729
			$newVersion = $this->schemaVersion;
1730
		}
1731
		
1732
		if( $version == $newVersion ) {
1733
			$result = file_get_contents( $filename );
1734
			
1735
			// remove unicode BOM if present
1736
			if( substr( $result, 0, 3 ) == sprintf( '%c%c%c', 239, 187, 191 ) ) {
1737
				$result = substr( $result, 3 );
1738
			}
1739
		} else {
1740
			$result = $this->TransformSchema( $filename, 'convert-' . $version . '-' . $newVersion, 'file' );
1741
		}
1742
		
1743
		if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) {
1744
			fwrite( $fp, $result );
1745
			fclose( $fp );
1746
		}
1747
		
1748
		return $result;
1749
	}
1750
	
1751
	function TransformSchema( $schema, $xsl, $schematype='string' )
1752
	{
1753
		// Fail if XSLT extension is not available
1754
		if( ! function_exists( 'xslt_create' ) ) {
1755
			return FALSE;
1756
		}
1757
		
1758
		$xsl_file = dirname( __FILE__ ) . '/xsl/' . $xsl . '.xsl';
1759
		
1760
		// look for xsl
1761
		if( !is_readable( $xsl_file ) ) {
1762
			return FALSE;
1763
		}
1764
		
1765
		switch( $schematype )
1766
		{
1767
			case 'file':
1768
				if( !is_readable( $schema ) ) {
1769
					return FALSE;
1770
				}
1771
				
1772
				$schema = file_get_contents( $schema );
1773
				break;
1774
			case 'string':
1775
			default:
1776
				if( !is_string( $schema ) ) {
1777
					return FALSE;
1778
				}
1779
		}
1780
		
1781
		$arguments = array (
1782
			'/_xml' => $schema,
1783
			'/_xsl' => file_get_contents( $xsl_file )
1784
		);
1785
		
1786
		// create an XSLT processor
1787
		$xh = xslt_create ();
1788
		
1789
		// set error handler
1790
		xslt_set_error_handler ($xh, array (&$this, 'xslt_error_handler'));
1791
		
1792
		// process the schema
1793
		$result = xslt_process ($xh, 'arg:/_xml', 'arg:/_xsl', NULL, $arguments); 
1794
		
1795
		xslt_free ($xh);
1796
		
1797
		return $result;
1798
	}
1799
	
1800
	/**
1801
	* Processes XSLT transformation errors
1802
	*
1803
	* @param object $parser XML parser object
1804
	* @param integer $errno Error number
1805
	* @param integer $level Error level
1806
	* @param array $fields Error information fields
1807
	*
1808
	* @access private
1809
	*/
1810
	function xslt_error_handler( $parser, $errno, $level, $fields ) {
1811
		if( is_array( $fields ) ) {
1812
			$msg = array(
1813
				'Message Type' => ucfirst( $fields['msgtype'] ),
1814
				'Message Code' => $fields['code'],
1815
				'Message' => $fields['msg'],
1816
				'Error Number' => $errno,
1817
				'Level' => $level
1818
			);
1819
			
1820
			switch( $fields['URI'] ) {
1821
				case 'arg:/_xml':
1822
					$msg['Input'] = 'XML';
1823
					break;
1824
				case 'arg:/_xsl':
1825
					$msg['Input'] = 'XSL';
1826
					break;
1827
				default:
1828
					$msg['Input'] = $fields['URI'];
1829
			}
1830
			
1831
			$msg['Line'] = $fields['line'];
1832
		} else {
1833
			$msg = array(
1834
				'Message Type' => 'Error',
1835
				'Error Number' => $errno,
1836
				'Level' => $level,
1837
				'Fields' => var_export( $fields, TRUE )
1838
			);
1839
		}
1840
		
1841
		$error_details = $msg['Message Type'] . ' in XSLT Transformation' . "\n"
1842
					   . '<table>' . "\n";
1843
		
1844
		foreach( $msg as $label => $details ) {
1845
			$error_details .= '<tr><td><b>' . $label . ': </b></td><td>' . htmlentities( $details ) . '</td></tr>' . "\n";
1846
		}
1847
		
1848
		$error_details .= '</table>';
1849
		
1850
		trigger_error( $error_details, E_USER_ERROR );
1851
	}
1852
	
1853
	/**
1854
	* Returns the AXMLS Schema Version of the requested XML schema file.
1855
	*
1856
	* Call this method to obtain the AXMLS DTD version of the requested XML schema file.
1857
	* @see SchemaStringVersion()
1858
	*
1859
	* @param string $filename AXMLS schema file
1860
	* @return string Schema version number or FALSE on error
1861
	*/
1862
	function SchemaFileVersion( $filename ) {
1863
		// Open the file
1864
		if( !($fp = fopen( $filename, 'r' )) ) {
1865
			// die( 'Unable to open file' );
1866
			return FALSE;
1867
		}
1868
		
1869
		// Process the file
1870
		while( $data = fread( $fp, 4096 ) ) {
1871
			if( preg_match( $this->versionRegex, $data, $matches ) ) {
1872
				return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION;
1873
			}
1874
		}
1875
		
1876
		return FALSE;
1877
	}
1878
	
1879
	/**
1880
	* Returns the AXMLS Schema Version of the provided XML schema string.
1881
	*
1882
	* Call this method to obtain the AXMLS DTD version of the provided XML schema string.
1883
	* @see SchemaFileVersion()
1884
	*
1885
	* @param string $xmlstring XML schema string
1886
	* @return string Schema version number or FALSE on error
1887
	*/
1888
	function SchemaStringVersion( $xmlstring ) {
1889
		if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) {
1890
			return FALSE;
1891
		}
1892
		
1893
		if( preg_match( $this->versionRegex, $xmlstring, $matches ) ) {
1894
			return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION;
1895
		}
1896
		
1897
		return FALSE;
1898
	}
1899
	
1900
	/**
1901
	* Extracts an XML schema from an existing database.
1902
	*
1903
	* Call this method to create an XML schema string from an existing database.
1904
	* If the data parameter is set to TRUE, AXMLS will include the data from the database
1905
	* in the schema. 
1906
	*
1907
	* @param boolean $data Include data in schema dump
1908
	* @return string Generated XML schema
1909
	*/
1910
	function ExtractSchema( $data = FALSE ) {
1911
		$old_mode = $this->db->SetFetchMode( ADODB_FETCH_NUM );
1912
		
1913
		$schema = '<?xml version="1.0"?>' . "\n"
1914
				. '<schema version="' . $this->schemaVersion . '">' . "\n";
1915
		
1916
		if( is_array( $tables = $this->db->MetaTables( 'TABLES' ) ) ) {
1917
			foreach( $tables as $table ) {
1918
				$schema .= '	<table name="' . $table . '">' . "\n";
1919
				
1920
				// grab details from database
1921
				$rs = $this->db->Execute( 'SELECT * FROM ' . $table . ' WHERE 1=1' );
1922
				$fields = $this->db->MetaColumns( $table );
1923
				$indexes = $this->db->MetaIndexes( $table );
1924
				
1925
				if( is_array( $fields ) ) {
1926
					foreach( $fields as $details ) {
1927
						$extra = '';
1928
						$content = array();
1929
						
1930
						if( $details->max_length > 0 ) {
1931
							$extra .= ' size="' . $details->max_length . '"';
1932
						}
1933
						
1934
						if( $details->primary_key ) {
1935
							$content[] = '<KEY/>';
1936
						} elseif( $details->not_null ) {
1937
							$content[] = '<NOTNULL/>';
1938
						}
1939
						
1940
						if( $details->has_default ) {
1941
							$content[] = '<DEFAULT value="' . $details->default_value . '"/>';
1942
						}
1943
						
1944
						if( $details->auto_increment ) {
1945
							$content[] = '<AUTOINCREMENT/>';
1946
						}
1947
						
1948
						// this stops the creation of 'R' columns,
1949
						// AUTOINCREMENT is used to create auto columns
1950
						$details->primary_key = 0;
1951
						$type = $rs->MetaType( $details );
1952
						
1953
						$schema .= '		<field name="' . $details->name . '" type="' . $type . '"' . $extra . '>';
1954
						
1955
						if( !empty( $content ) ) {
1956
							$schema .= "\n			" . implode( "\n			", $content ) . "\n		";
1957
						}
1958
						
1959
						$schema .= '</field>' . "\n";
1960
					}
1961
				}
1962
				
1963
				if( is_array( $indexes ) ) {
1964
					foreach( $indexes as $index => $details ) {
1965
						$schema .= '		<index name="' . $index . '">' . "\n";
1966
						
1967
						if( $details['unique'] ) {
1968
							$schema .= '			<UNIQUE/>' . "\n";
1969
						}
1970
						
1971
						foreach( $details['columns'] as $column ) {
1972
							$schema .= '			<col>' . $column . '</col>' . "\n";
1973
						}
1974
						
1975
						$schema .= '		</index>' . "\n";
1976
					}
1977
				}
1978
				
1979
				if( $data ) {
1980
					$rs = $this->db->Execute( 'SELECT * FROM ' . $table );
1981
					
1982
					if( is_object( $rs ) ) {
1983
						$schema .= '		<data>' . "\n";
1984
						
1985
						while( $row = $rs->FetchRow() ) {
1986
							foreach( $row as $key => $val ) {
1987
								$row[$key] = htmlentities($val);
1988
							}
1989
							
1990
							$schema .= '			<row><f>' . implode( '</f><f>', $row ) . '</f></row>' . "\n";
1991
						}
1992
						
1993
						$schema .= '		</data>' . "\n";
1994
					}
1995
				}
1996
				
1997
				$schema .= '	</table>' . "\n";
1998
			}
1999
		}
2000
		
2001
		$this->db->SetFetchMode( $old_mode );
2002
		
2003
		$schema .= '</schema>';
2004
		return $schema;
2005
	}
2006
	
2007
	/**
2008
	* Sets a prefix for database objects
2009
	*
2010
	* Call this method to set a standard prefix that will be prepended to all database tables 
2011
	* and indices when the schema is parsed. Calling setPrefix with no arguments clears the prefix.
2012
	*
2013
	* @param string $prefix Prefix that will be prepended.
2014
	* @param boolean $underscore If TRUE, automatically append an underscore character to the prefix.
2015
	* @return boolean TRUE if successful, else FALSE
2016
	*/
2017
	function SetPrefix( $prefix = '', $underscore = TRUE ) {
2018
		switch( TRUE ) {
2019
			// clear prefix
2020
			case empty( $prefix ):
2021
				logMsg( 'Cleared prefix' );
2022
				$this->objectPrefix = '';
2023
				return TRUE;
2024
			// prefix too long
2025
			case strlen( $prefix ) > XMLS_PREFIX_MAXLEN:
2026
			// prefix contains invalid characters
2027
			case !preg_match( '/^[a-z][a-z0-9_]+$/i', $prefix ):
2028
				logMsg( 'Invalid prefix: ' . $prefix );
2029
				return FALSE;
2030
		}
2031
		
2032
		if( $underscore AND substr( $prefix, -1 ) != '_' ) {
2033
			$prefix .= '_';
2034
		}
2035
		
2036
		// prefix valid
2037
		logMsg( 'Set prefix: ' . $prefix );
2038
		$this->objectPrefix = $prefix;
2039
		return TRUE;
2040
	}
2041
	
2042
	/**
2043
	* Returns an object name with the current prefix prepended.
2044
	*
2045
	* @param string	$name Name
2046
	* @return string	Prefixed name
2047
	*
2048
	* @access private
2049
	*/
2050
	function prefix( $name = '' ) {
2051
		// if prefix is set
2052
		if( !empty( $this->objectPrefix ) ) {
2053
			// Prepend the object prefix to the table name
2054
			// prepend after quote if used
2055
			return preg_replace( '/^(`?)(.+)$/', '$1' . $this->objectPrefix . '$2', $name );
2056
		}
2057
		
2058
		// No prefix set. Use name provided.
2059
		return $name;
2060
	}
2061
	
2062
	/**
2063
	* Checks if element references a specific platform
2064
	*
2065
	* @param string $platform Requested platform
2066
	* @returns boolean TRUE if platform check succeeds
2067
	*
2068
	* @access private
2069
	*/
2070
	function supportedPlatform( $platform = NULL ) {
2071
		$regex = '/^(\w*\|)*' . $this->db->databaseType . '(\|\w*)*$/';
2072
		
2073
		if( !isset( $platform ) OR preg_match( $regex, $platform ) ) {
2074
			logMsg( "Platform $platform is supported" );
2075
			return TRUE;
2076
		} else {
2077
			logMsg( "Platform $platform is NOT supported" );
2078
			return FALSE;
2079
		}
2080
	}
2081
	
2082
	/**
2083
	* Clears the array of generated SQL.
2084
	*
2085
	* @access private
2086
	*/
2087
	function clearSQL() {
2088
		$this->sqlArray = array();
2089
	}
2090
	
2091
	/**
2092
	* Adds SQL into the SQL array.
2093
	*
2094
	* @param mixed $sql SQL to Add
2095
	* @return boolean TRUE if successful, else FALSE.
2096
	*
2097
	* @access private
2098
	*/	
2099
	function addSQL( $sql = NULL ) {
2100
		if( is_array( $sql ) ) {
2101
			foreach( $sql as $line ) {
2102
				$this->addSQL( $line );
2103
			}
2104
			
2105
			return TRUE;
2106
		}
2107
		
2108
		if( is_string( $sql ) ) {
2109
			$this->sqlArray[] = $sql;
2110
			
2111
			// if executeInline is enabled, and either no errors have occurred or continueOnError is enabled, execute SQL.
2112
			if( $this->ExecuteInline() && ( $this->success == 2 || $this->ContinueOnError() ) ) {
2113
				$saved = $this->db->debug;
2114
				$this->db->debug = $this->debug;
2115
				$ok = $this->db->Execute( $sql );
2116
				$this->db->debug = $saved;
2117
				
2118
				if( !$ok ) {
2119
					if( $this->debug ) {
2120
						ADOConnection::outp( $this->db->ErrorMsg() );
2121
					}
2122
					
2123
					$this->success = 1;
2124
				}
2125
			}
2126
			
2127
			return TRUE;
2128
		}
2129
		
2130
		return FALSE;
2131
	}
2132
	
2133
	/**
2134
	* Gets the SQL array in the specified format.
2135
	*
2136
	* @param string $format Format
2137
	* @return mixed SQL
2138
	*	
2139
	* @access private
2140
	*/
2141
	function getSQL( $format = NULL, $sqlArray = NULL ) {
2142
		if( !is_array( $sqlArray ) ) {
2143
			$sqlArray = $this->sqlArray;
2144
		}
2145
		
2146
		if( !is_array( $sqlArray ) ) {
2147
			return FALSE;
2148
		}
2149
		
2150
		switch( strtolower( $format ) ) {
2151
			case 'string':
2152
			case 'text':
2153
				return !empty( $sqlArray ) ? implode( ";\n\n", $sqlArray ) . ';' : '';
2154
			case'html':
2155
				return !empty( $sqlArray ) ? nl2br( htmlentities( implode( ";\n\n", $sqlArray ) . ';' ) ) : '';
2156
		}
2157
		
2158
		return $this->sqlArray;
2159
	}
2160
	
2161
	/**
2162
	* Destroys an adoSchema object.
2163
	*
2164
	* Call this method to clean up after an adoSchema object that is no longer in use.
2165
	* @deprecated adoSchema now cleans up automatically.
2166
	*/
2167
	function Destroy() {
2168
		set_magic_quotes_runtime( $this->mgq );
2169
		unset( $this );
2170
	}
2171
}
2172

    
2173
/**
2174
* Message logging function
2175
*
2176
* @access private
2177
*/
2178
function logMsg( $msg, $title = NULL, $force = FALSE ) {
2179
	if( XMLS_DEBUG or $force ) {
2180
		echo '<pre>';
2181
		
2182
		if( isset( $title ) ) {
2183
			echo '<h3>' . htmlentities( $title ) . '</h3>';
2184
		}
2185
		
2186
		if( is_object( $this ) ) {
2187
			echo '[' . get_class( $this ) . '] ';
2188
		}
2189
		
2190
		print_r( $msg );
2191
		
2192
		echo '</pre>';
2193
	}
2194
}
2195
?>
(14-14/23)