More refactoring and code tidy. We now have get_attributes and
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Parser / XML / XMI.pm
CommitLineData
1223c9b2 1package SQL::Translator::Parser::XML::XMI;
2
3# -------------------------------------------------------------------
91ed9c32 4# $Id: XMI.pm,v 1.4 2003-09-09 01:00:44 grommit Exp $
1223c9b2 5# -------------------------------------------------------------------
6# Copyright (C) 2003 Mark Addison <mark.addison@itn.co.uk>,
7#
8# This program is free software; you can redistribute it and/or
9# modify it under the terms of the GNU General Public License as
10# published by the Free Software Foundation; version 2.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
20# 02111-1307 USA
21# -------------------------------------------------------------------
22
23=head1 NAME
24
25SQL::Translator::Parser::XML::XMI - Parser to create Schema from UML
26Class diagrams stored in XMI format.
27
f8ec05fa 28=head1 SYNOPSIS
29
30 use SQL::Translator;
31 use SQL::Translator::Parser::XML::XMI;
32
33 my $translator = SQL::Translator->new(
34 from => 'XML-XMI',
35 to => 'MySQL',
36 filename => 'schema.xmi',
37 show_warnings => 1,
38 add_drop_table => 1,
39 );
40
41 print $obj->translate;
42
43=head1 DESCRIPTION
44
45=head2 UML Data Modeling
46
47To tell the parser which Classes are tables give them a <<Table>> stereotype.
48
49Any attributes of the class will be used as fields. The datatype of the
50attribute must be a UML datatype and not an object, with the datatype's name
51being used to set the data_type value in the schema.
52
53Primary keys are attributes marked with <<PK>> stereotype.
54
55=head2 XMI Format
56
57The parser has been built using XMI generated by PoseidonUML 2beta, which
58says it uses UML 2. So the current conformance is down to Poseidon's idea
59of XMI!
60
61=head1 ARGS
62
63=over 4
64
65=item visibility
66
67 visibilty=public|protected|private
68
69What visibilty of stuff to translate. e.g when set to 'public' any private
70and package Classes will be ignored and not turned into tables. Applies
71to Classes and Attributes.
72
73If not set or false (the default) no checks will be made and everything is
74translated.
75
76=back
77
1223c9b2 78=cut
79
80# -------------------------------------------------------------------
81
82use strict;
83
84use vars qw[ $DEBUG $VERSION @EXPORT_OK ];
91ed9c32 85$VERSION = sprintf "%d.%02d", q$Revision: 1.4 $ =~ /(\d+)\.(\d+)/;
1223c9b2 86$DEBUG = 0 unless defined $DEBUG;
87
88use Data::Dumper;
89use Exporter;
90use base qw(Exporter);
91@EXPORT_OK = qw(parse);
92
93use base qw/SQL::Translator::Parser/; # Doesnt do anything at the mo!
94use SQL::Translator::Utils 'debug';
95use XML::XPath;
96use XML::XPath::XMLParser;
97
91ed9c32 98# XMI XPath parsing
99#-----------------------------------------------------------------------------
100
1223c9b2 101#
f8ec05fa 102# get_classes( XPATHOBJ, ARGS );
1223c9b2 103#
f8ec05fa 104# XPATHOBJ - An XML::XPath object setup and ready to use. You can also use any
105# Node to search from as this sub just calls findnodes() on the arg.
ef2d7798 106#
f8ec05fa 107# ARGS - Name/Value list of args.
ef2d7798 108#
f8ec05fa 109# xpath => The xpath to use for finding classes. Default is //UML:Classes
110# which will find all the classes in the XMI.
ef2d7798 111#
91ed9c32 112# test => An XPath predicate (ie the bit between [] ) to test the
113# classes with to decide if we should parse them. ie
114# test => '@name' would only pass out classes with a name.
115# Can also give it an array ref and it will and the tests.
116# It gets tacked onto to xpath so don't put any [] on
117# xpath if you use test as well.
118#
119# attribute_test => An XPath predicate to pass onto get_attributes.
f8ec05fa 120#
91ed9c32 121
122# _add_xpath_tests $path, [qw/@name xmiVisible("public")/]; # and
123# _add_xpath_tests $path, [qw/@name xmiVisible("public")/], "or";
124sub _add_xpath_tests {
125 my ($path,$tests,$join) = @_;
126 return $path unless defined $tests;
127 my @tests = ref($tests) ? @$tests : $tests;
128 return $path unless @tests;
129 $join ||= "and";
130 return $path."[".join(" $join ", @tests)."]";
131}
132
133sub get_stereotype {
134 my ($xp) = @_;
135 return "".$xp->findvalue(
136 'xmiDeref(UML:ModelElement.stereotype/UML:Stereotype)/@name');
137 # TODO Test for difference between it existing or being "" ?
138}
139
f8ec05fa 140sub get_classes {
141 my ($xp,%args) = @_;
91ed9c32 142 my $classes;
f8ec05fa 143
144 my $xpath = $args{xpath} ||= '//UML:Class'; # Default: all classes
91ed9c32 145 $xpath = _add_xpath_tests $xpath, $args{test};
146 debug "Searching for Classes using:$xpath";
f8ec05fa 147
148 my @nodes = $xp->findnodes($xpath);
149 return unless @nodes;
150
151 for my $classnode (@nodes) {
152 my $class = {};
91ed9c32 153
154 foreach (
f8ec05fa 155 qw/name visibility isSpecification
156 isRoot isLeaf isAbstract isActive/
157 ) {
158 $class->{$_} = $classnode->getAttribute($_);
159 }
91ed9c32 160 $class->{stereotype} = get_stereotype($classnode);
f8ec05fa 161
f8ec05fa 162 # Class Attributes
f8ec05fa 163 my $xpath = 'UML:Classifier.feature/UML:Attribute';
91ed9c32 164 $class->{attributes} = get_attributes( $classnode,
165 xpath => $xpath, test => $args{attribute_test} );
166
f8ec05fa 167 push @$classes, $class;
168 }
91ed9c32 169 return wantarray ? @$classes : $classes;
f8ec05fa 170};
ef2d7798 171
91ed9c32 172sub get_attributes {
173 my ($xp, %args) = @_;
174
175 my $xpath = $args{xpath} ||= '//UML:Classifier.feature/UML:Attribute';
176 $xpath = _add_xpath_tests $xpath, $args{test};
177 debug "Searching for Attributes using:$xpath";
178
179 my $attributes;
180 foreach my $node ( $xp->findnodes($xpath) ) {
181 my $attr = {};
182
183 foreach (qw/name visibility isSpecification ownerScope/) {
184 $attr->{$_} = $node->getAttribute($_);
185 }
186 $attr->{stereotype} = get_stereotype($node);
187
188 # Get datatype name and the name body of the initial value
189 $attr->{datatype} = "".$node->find(
190 'xmiDeref(UML:StructuralFeature.type/UML:DataType)/@name');
191 if ( my @body = $node->findnodes(
192 'UML:Attribute.initialValue/UML:Expression/@body')
193 ) {
194 $attr->{initialValue} = $body[0]->getData;
195 }
196
197 push @$attributes, $attr;
198 }
199 return wantarray ? @$attributes : $attributes;
200}
201
202
203
204# SQLFairy Parser
205#-----------------------------------------------------------------------------
206
1223c9b2 207sub parse {
f8ec05fa 208 eval {
209
1223c9b2 210 my ( $translator, $data ) = @_;
211 local $DEBUG = $translator->debug;
212 my $schema = $translator->schema;
ef2d7798 213 my $pargs = $translator->parser_args;
1223c9b2 214
ef2d7798 215 debug "Visibility Level:$pargs->{visibility}" if $DEBUG;
216
217 my $xp = XML::XPath->new(xml => $data);
1223c9b2 218 $xp->set_namespace("UML", "org.omg.xmi.namespace.UML");
219 #
220 # TODO
221 # - Options to set the initial context node so we don't just
222 # blindly do all the classes. e.g. Select a diag name to do.
ef2d7798 223
f8ec05fa 224 #
225 # Build an XPath for the classes and attributes we want...
226 #
91ed9c32 227 # Only classes with an id (so we don't get any refs to classes ie
228 # xmi.idref classes). They also need a name to be usefull.
229 my @tests = ('@xmi.id and @name');
f8ec05fa 230 push @tests, "xmiVisible('$pargs->{visibility}')" if $pargs->{visibility};
f8ec05fa 231
91ed9c32 232 my $attrib_test = '@name and @xmi.id';
f8ec05fa 233 $attrib_test .= " and xmiVisible('$pargs->{visibility}')"
234 if $pargs->{visibility};
235
236 # ...and parse them out
f8ec05fa 237 my $classes = get_classes( $xp,
91ed9c32 238 xpath => "//UML:Class", test => [@tests], attribute_test => $attrib_test);
f8ec05fa 239
240 debug "Found ".scalar(@$classes)." Classes: ".join(", ",
241 map {$_->{"name"}} @$classes) if $DEBUG;
242 debug "Classes:",Dumper($classes);
243
244 #
245 # Turn the data from get_classes into a Schema
246 #
247 foreach my $class (@$classes) {
248 next unless $class->{stereotype} eq "Table";
ef2d7798 249
1223c9b2 250 # Add the table
f8ec05fa 251 debug "Adding class: $class->{name}" if $DEBUG;
252 my $table = $schema->add_table( name => $class->{name} )
1223c9b2 253 or die "Schema Error: ".$schema->error;
254
255 #
256 # Fields from Class attributes
257 #
ef2d7798 258 # name data_type size default_value is_nullable
1223c9b2 259 # is_auto_increment is_primary_key is_foreign_key comments
260 #
f8ec05fa 261 foreach my $attr ( @{$class->{attributes}} ) {
262 my %data = (
263 name => $attr->{name},
264 data_type => $attr->{datatype},
265 is_primary_key => $attr->{stereotype} eq "PK" ? 1 : 0,
1223c9b2 266 #is_foreign_key => $stereotype eq "FK" ? 1 : 0,
267 );
f8ec05fa 268 $data{default_value} = $attr->{initialValue}
269 if exists $attr->{initialValue};
1223c9b2 270
271 debug "Adding field:",Dumper(\%data);
272 my $field = $table->add_field( %data ) or die $schema->error;
273
274 $table->primary_key( $field->name ) if $data{'is_primary_key'};
275 #
276 # TODO:
ef2d7798 277 # - We should be able to make the table obj spot this when
1223c9b2 278 # we use add_field.
279 #
280 }
281
282 } # Classes loop
283
284 return 1;
f8ec05fa 285
286 };
287 print "ERROR: $@\n" if $@;
288 return 1;
1223c9b2 289}
290
2911;
292
f8ec05fa 293#=============================================================================
294#
295# XML::XPath extensions
296#
297#=============================================================================
1223c9b2 298
f8ec05fa 299package XML::XPath::Function;
1223c9b2 300
f8ec05fa 301=head1 XMI XPath Functions
1223c9b2 302
f8ec05fa 303The Parser adds the following extra XPath functions.
1223c9b2 304
f8ec05fa 305=head2 xmiDeref
1223c9b2 306
f8ec05fa 307Deals with xmi.id/xmi.idref pairs of attributes. You give it an
308xPath e.g 'UML:ModelElement.stereotype/UML:stereotype' if the the
309tag it points at has an xmi.idref it looks up the tag with that
310xmi.id and returns it.
1223c9b2 311
f8ec05fa 312If it doesn't have an xmi.id, the path is returned as normal.
1223c9b2 313
f8ec05fa 314e.g. given
1223c9b2 315
f8ec05fa 316 <UML:ModelElement.stereotype>
317 <UML:Stereotype xmi.idref = 'stTable'/>
318 </UML:ModelElement.stereotype>
319 ...
320 <UML:Stereotype xmi.id='stTable' name='Table' visibility='public'
321 isAbstract='false' isSpecification='false' isRoot='false' isLeaf='false'>
322 <UML:Stereotype.baseClass>Class</UML:Stereotype.baseClass>
323 </UML:Stereotype>
1223c9b2 324
f8ec05fa 325Using xmideref(//UML:ModelElement.stereotype/UML:stereotype) would return the
326<UML:Stereotype xmi.id = '3b4b1e:f762a35f6b:-7fb6' ...> tag.
1223c9b2 327
f8ec05fa 328Using xmideref(//UML:ModelElement.stereotype/UML:stereotype)/@name would give
329"Table".
1223c9b2 330
f8ec05fa 331=head xmiVisible
1223c9b2 332
f8ec05fa 333 is_visible( VISLEVEL )
1223c9b2 334
f8ec05fa 335Returns true or false for whether the visibility of something e.g. a Class or
336Attribute, is visible at the level given. e.g.
1223c9b2 337
f8ec05fa 338 //UML:Class[xmiVisible('public')] - Find all public classes
339 //UML:Class[xmiVisible('protected')] - Find all public and protected classes
1223c9b2 340
f8ec05fa 341Supports the 3 UML visibility levels of public, protected and private.
1223c9b2 342
f8ec05fa 343Note: Currently any element tested that doesn't have a visibility="" attribute
344is assumed to be visible and so xmiVisible will return true. This is probably
345the wrong thing to do and is very likley to change. It is probably best to
346throw an error if we try to test something that doesn't do visibility.
1223c9b2 347
f8ec05fa 348=cut
1223c9b2 349
f8ec05fa 350sub xmiDeref {
351 my $self = shift;
352 my ($node, @params) = @_;
353 if (@params > 1) {
354 die "xmiDeref() function takes one or no parameters\n";
355 }
356 elsif (@params) {
357 my $nodeset = shift(@params);
358 return $nodeset unless $nodeset->size;
359 $node = $nodeset->get_node(1);
360 }
361 die "xmiDeref() needs an Element node."
362 unless $node->isa("XML::XPath::Node::Element");
1223c9b2 363
f8ec05fa 364 my $id = $node->getAttribute("xmi.idref") or return $node;
365 return $node->getRootNode->find('//*[@xmi.id="'.$id.'"]');
366}
367
368{
369 my %vislevel = (
370 public => 1,
371 protected => 2,
372 private => 3,
373 );
374
375 sub xmiVisible {
376 my $self = shift;
377 my ($node, @params) = @_;
378 if (@params < 1 or @params > 2) {
379 die "xmiVisible() function takes 1 or 2 parameters\n";
380 }
381 elsif (@params == 2) {
382 my $nodeset = shift(@params);
383 return unless $nodeset->size;
384 $node = $nodeset->get_node(1);
385 }
386 die "xmiVisible() needs an Element node."
387 unless $node->isa("XML::XPath::Node::Element");
388
389 my $vis = shift(@params) || return XML::XPath::Boolean->True;
390 my $nodevis = $node->getAttribute("visibility")
391 || return XML::XPath::Boolean->True;
392 return XML::XPath::Boolean->True
393 if $vislevel{$vis} >= $vislevel{$nodevis};
394 return XML::XPath::Boolean->False;
395 }
396}
397
398# Test of custom xpath function.
399sub hello {
400 return XML::XPath::Literal->new("Hello World");
401}
402
403#=============================================================================
404package main;
405
406
407=pod
1223c9b2 408
409=head1 BUGS
410
ef2d7798 411Seems to be slow. I think this is because the XMI files can get pretty
412big and complex, especially all the diagram info.
413
1223c9b2 414=head1 TODO
415
ef2d7798 416B<field sizes> Don't think UML does this directly so may need to include
1223c9b2 417it in the datatype names.
418
ef2d7798 419B<table_visibility and field_visibility args> Seperate control over what is
420parsed, setting visibility arg will set both.
421
1223c9b2 422Everything else! Relations, fkeys, constraints, indexes, etc...
423
424=head1 AUTHOR
425
426Mark D. Addison E<lt>mark.addison@itn.co.ukE<gt>.
427
428=head1 SEE ALSO
429
430perl(1), SQL::Translator, XML::XPath, SQL::Translator::Producer::XML::SQLFairy,
431SQL::Translator::Schema.
432
433=cut
f8ec05fa 434
435