Added writing of extra data for all objects to XML producer.
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / XML / SQLFairy.pm
1 package SQL::Translator::Producer::XML::SQLFairy;
2
3 # -------------------------------------------------------------------
4 # $Id: SQLFairy.pm,v 1.19 2004-11-05 16:37:00 grommit Exp $
5 # -------------------------------------------------------------------
6 # Copyright (C) 2003 Ken Y. Clark <kclark@cpan.org>,
7 #                    darren chamberlain <darren@cpan.org>,
8 #                    Chris Mungall <cjm@fruitfly.org>,
9 #                    Mark Addison <mark.addison@itn.co.uk>.
10 #
11 # This program is free software; you can redistribute it and/or
12 # modify it under the terms of the GNU General Public License as
13 # published by the Free Software Foundation; version 2.
14 #
15 # This program is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 # General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
23 # 02111-1307  USA
24 # -------------------------------------------------------------------
25
26 =pod
27
28 =head1 NAME
29
30 SQL::Translator::Producer::XML::SQLFairy - SQLFairy's default XML format
31
32 =head1 SYNOPSIS
33
34   use SQL::Translator;
35
36   my $t              = SQL::Translator->new(
37       from           => 'MySQL',
38       to             => 'XML-SQLFairy',
39       filename       => 'schema.sql',
40       show_warnings  => 1,
41   );
42
43   print $t->translate;
44
45 =head1 DESCRIPTION
46
47 Creates XML output of a schema, in the flavor of XML used natively by the
48 SQLFairy project (L<SQL::Translator>). This format is detailed here.
49
50 The XML lives in the C<http://sqlfairy.sourceforge.net/sqlfairy.xml> namespace.
51 With a root element of <schema>.
52
53 Objects in the schema are mapped to tags of the same name as the objects class
54 (all lowercase).
55
56 The attributes of the objects (e.g. $field->name) are mapped to attributes of
57 the tag, except for sql, comments and action, which get mapped to child data
58 elements.
59
60 List valued attributes (such as the list of fields in an index)
61 get mapped to comma seperated lists of values in the attribute.
62
63 Child objects, such as a tables fields, get mapped to child tags wrapped in a
64 set of container tags using the plural of their contained classes name.
65
66 An objects's extra attribute (a hash of arbitary data) is
67 mapped to a tag called extra, with the hash of data as attributes, sorted into
68 alphabetical order.
69
70 e.g.
71
72     <schema name="" database=""
73       xmlns="http://sqlfairy.sourceforge.net/sqlfairy.xml">
74
75       <tables>
76         <table name="Story" order="1">
77           <fields>
78             <field name="id" data_type="BIGINT" size="20"
79               is_nullable="0" is_auto_increment="1" is_primary_key="1"
80               is_foreign_key="0" order="3">
81               <extra ZEROFILL="1" />
82               <comments></comments>
83             </field>
84             <field name="created" data_type="datetime" size="0"
85               is_nullable="1" is_auto_increment="0" is_primary_key="0"
86               is_foreign_key="0" order="1">
87               <extra />
88               <comments></comments>
89             </field>
90             ...
91           </fields>
92           <indices>
93             <index name="foobar" type="NORMAL" fields="foo,bar" options="" />
94           </indices>
95         </table>
96       </tables>
97
98       <views>
99         <view name="email_list" fields="email" order="1">
100           <sql>SELECT email FROM Basic WHERE email IS NOT NULL</sql>
101         </view>
102       </views>
103
104     </schema>
105
106 To see a complete example of the XML translate one of your schema :)
107
108   $ sqlt -f MySQL -t XML-SQLFairy schema.sql
109
110 =head1 ARGS
111
112 =over 4
113
114 =item add_prefix
115
116 Set to true to use the default namespace prefix of 'sqlf', instead of using
117 the default namespace for
118 C<http://sqlfairy.sourceforge.net/sqlfairy.xml namespace>
119
120 e.g.
121
122  <!-- add_prefix=0 -->
123  <field name="foo" />
124
125  <!-- add_prefix=1 -->
126  <sqlf:field name="foo" />
127
128 =item prefix
129
130 Set to the namespace prefix you want to use for the
131 C<http://sqlfairy.sourceforge.net/sqlfairy.xml namespace>
132
133 e.g.
134
135  <!-- prefix='foo' -->
136  <foo:field name="foo" />
137
138 =item newlines
139
140 If true (the default) inserts newlines around the XML, otherwise the schema is
141 written on one line.
142
143 =item indent
144
145 When using newlines the number of whitespace characters to use as the indent.
146 Default is 2, set to 0 to turn off indenting.
147
148 =back
149
150 =head1 LEGACY FORMAT
151
152 The previous version of the SQLFairy XML allowed the attributes of the the
153 schema objects to be written as either xml attributes or as data elements, in
154 any combination. The old producer could produce attribute only or data element
155 only versions. While this allowed for lots of flexibility in writing the XML
156 the result is a great many possible XML formats, not so good for DTD writing,
157 XPathing etc! So we have moved to a fixed version described above.
158
159 This version of the producer will now only produce the new style XML.
160 To convert your old format files simply pass them through the translator :)
161
162  $ sqlt -f XML-SQLFairy -t XML-SQLFairy schema-old.xml > schema-new.xml
163
164 =cut
165
166 use strict;
167 use vars qw[ $VERSION @EXPORT_OK ];
168 $VERSION = sprintf "%d.%02d", q$Revision: 1.19 $ =~ /(\d+)\.(\d+)/;
169
170 use Exporter;
171 use base qw(Exporter);
172 @EXPORT_OK = qw(produce);
173
174 use IO::Scalar;
175 use SQL::Translator::Utils qw(header_comment debug);
176 BEGIN {
177     # Will someone fix XML::Writer already?
178     local $^W = 0;
179     require XML::Writer;
180     import XML::Writer;
181 }
182
183 # Which schema object attributes (methods) to write as xml elements rather than
184 # as attributes. e.g. <comments>blah, blah...</comments>
185 my @MAP_AS_ELEMENTS = qw/sql comments action extra/;
186
187
188
189 my $Namespace = 'http://sqlfairy.sourceforge.net/sqlfairy.xml';
190 my $Name      = 'sqlf';
191 my $PArgs     = {};
192
193 sub produce {
194     my $translator  = shift;
195     my $schema      = $translator->schema;
196     $PArgs          = $translator->producer_args;
197     my $newlines    = defined $PArgs->{newlines} ? $PArgs->{newlines} : 1;
198     my $indent      = defined $PArgs->{indent}   ? $PArgs->{indent}   : 2;
199     my $io          = IO::Scalar->new;
200
201     # Setup the XML::Writer and set the namespace
202     my $prefix = "";
203     $prefix    = $Name            if $PArgs->{add_prefix};
204     $prefix    = $PArgs->{prefix} if $PArgs->{prefix};
205     my $xml         = XML::Writer->new(
206         OUTPUT      => $io,
207         NAMESPACES  => 1,
208         PREFIX_MAP  => { $Namespace => $prefix },
209         DATA_MODE   => $newlines,
210         DATA_INDENT => $indent,
211     );
212
213     # Start the document
214     $xml->xmlDecl('UTF-8');
215     $xml->comment(header_comment('', ''));
216     xml_obj($xml, $schema,
217         tag => "schema", methods => [qw/name database extra/], end_tag => 0 );
218
219     #
220     # Table
221     #
222     $xml->startTag( [ $Namespace => "tables" ] );
223     for my $table ( $schema->get_tables ) {
224         debug "Table:",$table->name;
225         xml_obj($xml, $table,
226              tag => "table",
227              methods => [qw/name order extra/],
228              end_tag => 0
229          );
230
231         #
232         # Fields
233         #
234         xml_obj_children( $xml, $table,
235             tag   => 'field',
236             methods =>[qw/
237                 name data_type size is_nullable default_value is_auto_increment
238                 is_primary_key is_foreign_key extra comments order
239             /],
240         );
241
242         #
243         # Indices
244         #
245         xml_obj_children( $xml, $table,
246             tag   => 'index',
247             collection_tag => "indices",
248             methods => [qw/name type fields options extra/],
249         );
250
251         #
252         # Constraints
253         #
254         xml_obj_children( $xml, $table,
255             tag   => 'constraint',
256             methods => [qw/
257                 name type fields reference_table reference_fields
258                 on_delete on_update match_type expression options deferrable
259                 extra
260             /],
261         );
262
263         $xml->endTag( [ $Namespace => 'table' ] );
264     }
265     $xml->endTag( [ $Namespace => 'tables' ] );
266
267     #
268     # Views
269     #
270     xml_obj_children( $xml, $schema,
271         tag   => 'view',
272         methods => [qw/name sql fields order extra/],
273     );
274
275     #
276     # Tiggers
277     #
278     xml_obj_children( $xml, $schema,
279         tag    => 'trigger',
280         methods => [qw/name database_event action on_table perform_action_when
281             fields order extra/],
282     );
283
284     #
285     # Procedures
286     #
287     xml_obj_children( $xml, $schema,
288         tag   => 'procedure',
289         methods => [qw/name sql parameters owner comments order extra/],
290     );
291
292     $xml->endTag([ $Namespace => 'schema' ]);
293     $xml->end;
294
295     return $io;
296 }
297
298
299 #
300 # Takes and XML::Write object, Schema::* parent object, the tag name,
301 # the collection name and a list of methods (of the children) to write as XML.
302 # The collection name defaults to the name with an s on the end and is used to
303 # work out the method to get the children with. eg a name of 'foo' gives a
304 # collection of foos and gets the members using ->get_foos.
305 #
306 sub xml_obj_children {
307     my ($xml,$parent) = (shift,shift);
308     my %args = @_;
309     my ($name,$collection_name,$methods)
310         = @args{qw/tag collection_tag methods/};
311     $collection_name ||= "${name}s";
312     my $meth = "get_$collection_name";
313
314     my @kids = $parent->$meth;
315     #@kids || return;
316     $xml->startTag( [ $Namespace => $collection_name ] );
317     for my $obj ( @kids ) {
318         xml_obj($xml, $obj,
319             tag     => "$name",
320             end_tag => 1,
321             methods => $methods,
322         );
323     }
324     $xml->endTag( [ $Namespace => $collection_name ] );
325 }
326
327 #
328 # Takes an XML::Writer, Schema::* object and list of method names
329 # and writes the obect out as XML. All methods values are written as attributes
330 # except for the methods listed in @MAP_AS_ELEMENTS which get written as child
331 # data elements.
332 #
333 # The attributes/tags are written in the same order as the method names are
334 # passed.
335 #
336 # TODO
337 # - Should the Namespace be passed in instead of global? Pass in the same
338 #   as Writer ie [ NS => TAGNAME ]
339 #
340 my $elements_re = join("|", @MAP_AS_ELEMENTS);
341 $elements_re = qr/^($elements_re)$/;
342 sub xml_obj {
343     my ($xml, $obj, %args) = @_;
344     my $tag                = $args{'tag'}              || '';
345     my $end_tag            = $args{'end_tag'}          || '';
346     my @meths              = @{ $args{'methods'} };
347     my $empty_tag          = 0;
348
349     # Use array to ensure consistant (ie not hash) ordering of attribs
350     # The order comes from the meths list passed in.
351     my @tags;
352     my @attr;
353     foreach ( grep { defined $obj->$_ } @meths ) {
354         my $what = m/$elements_re/ ? \@tags : \@attr;
355         my $val = $_ eq 'extra'
356             ? { $obj->$_ }
357             : $obj->$_;
358         $val = ref $val eq 'ARRAY' ? join(',', @$val) : $val;
359         push @$what, $_ => $val;
360     };
361     my $child_tags = @tags;
362     $end_tag && !$child_tags
363         ? $xml->emptyTag( [ $Namespace => $tag ], @attr )
364         : $xml->startTag( [ $Namespace => $tag ], @attr );
365     while ( my ($name,$val) = splice @tags,0,2 ) {
366         if ( ref $val eq 'HASH' ) {
367              $xml->emptyTag( [ $Namespace => $name ],
368                  map { ($_, $val->{$_}) } sort keys %$val );
369         }
370         else {
371             $xml->dataElement( [ $Namespace => $name ], $val );
372         }
373     }
374     $xml->endTag( [ $Namespace => $tag ] ) if $child_tags && $end_tag;
375 }
376
377 1;
378
379 # -------------------------------------------------------------------
380 # The eyes of fire, the nostrils of air,
381 # The mouth of water, the beard of earth.
382 # William Blake
383 # -------------------------------------------------------------------
384
385 =pod
386
387 =head1 AUTHORS
388
389 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>,
390 Darren Chamberlain E<lt>darren@cpan.orgE<gt>,
391 Mark Addison E<lt>mark.addison@itn.co.ukE<gt>.
392
393 =head1 SEE ALSO
394
395 L<perl(1)>, L<SQL::Translator>, L<SQL::Translator::Parser::XML::SQLFairy>,
396 L<SQL::Translator::Schema>, L<XML::Writer>.
397
398 =cut