# $Author: ddumont $ # $Date: 2009-05-30 17:54:42 +0200 (sam 30 mai 2009) $ # $Revision: 970 $ # Copyright (c) 2007-2009 Dominique Dumont. # # This file is part of Config-Model-Itself. # # Config-Xorg is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser Public License as # published by the Free Software Foundation; either version 2.1 of # the License, or (at your option) any later version. # # Config-Xorg is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with Config-Model; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA package Config::Model::Itself ; use strict; use warnings ; use Carp ; use IO::File ; use Log::Log4perl; use Data::Dumper ; use File::Find ; use File::Path ; use File::Basename ; use vars qw($VERSION) ; $VERSION = '1.211'; my $logger = Log::Log4perl::get_logger(__PACKAGE__); =head1 NAME Config::Model::Itself - Model editor for Config::Model =head1 SYNOPSIS my $meta_model = Config::Model -> new ( ) ; # load Config::Model model my $meta_inst = $model->instance (root_class_name => 'Itself::Model' , instance_name => 'meta_model' , ); my $meta_root = $meta_inst -> config_root ; # Itself constructor returns an object to read or write the data # structure containing the model to be edited my $rw_obj = Config::Model::Itself -> new(model_object => $meta_root ) ; # now lead the model to be edited $rw_obj -> read_all( conf_dir => '/path/to/model_files') ; # For Curses UI prepare a call-back to write model my $wr_back = sub { $rw_obj->write_all(conf_dir => '/path/to/model_files'); # create Curses user interface my $dialog = Config::Model::CursesUI-> new ( experience => 'advanced', store => $wr_back, ) ; # start Curses dialog to edit the mode $dialog->start( $meta_model ) ; # that's it. When user quits curses interface, Curses will call # $wr_back sub ref to write the modified model. =head1 DESCRIPTION The Config::Itself and its model files provide a model of Config:Model (hence the Itself name). Let's step back a little to explain. Any configuration data is, in essence, structured data. This data could be stored in an XML file. A configuration model is a way to describe the structure and relation of all items of a configuration data set. This configuration model is also expressed as structured data. This structure data is structured and follow a set of rules which are described for humans in L. The structure and rules documented in L are also expressed in a model in the files provided with C. Hence the possibity to verify, modify configuration data provided by Config::Model can also be applied on configuration models. Using the same user interface. From a Perl point of view, Config::Model::Itself provides a class dedicated to read and write a set of model files. =head1 Constructor =head2 new ( model_object => ... ) Creates a new read/write handler. This handler is dedicated to the C passed with the constructor. This parameter must be a L class. =cut # find all .pl file in model_dir and load them... sub new { my $type = shift ; my %args = @_ ; my $model_obj = $args{model_object} || croak __PACKAGE__," read_all: undefined model object"; croak __PACKAGE__," read_all: model_object is not a Config::Model::Node object" unless $model_obj->isa("Config::Model::Node"); bless { model_object => $model_obj }, $type ; } =head2 Methods =head1 read_all ( model_dir => ... , root_model => ... , [ force_load => 1 ] ) Load all the model files contained in C and all its subdirectories. C is used to filter the classes read. Use C if you are trying to load a model containing errors. C returns a hash ref containing ( class_name => file_name , ...) =cut sub read_all { my $self = shift ; my %args = @_ ; my $model_obj = $self->{model_object}; my $dir = $args{model_dir} || croak __PACKAGE__," read_all: undefined config dir"; my $model = $args{root_model} || croak __PACKAGE__," read_all: undefined root_model"; my $force_load = $args{force_load} || 0 ; my $legacy = $args{legacy} ; unless (-d $dir ) { croak __PACKAGE__," read_all: unknown config dir $dir"; } my @files ; my $wanted = sub { my $n = $File::Find::name ; push @files, $n if (-f $_ and not /~$/ and $n !~ /CVS/ and $n !~ m!.svn! and $n =~ /\b$model/ ) ; } ; find ($wanted, $dir ) ; my $i = $model_obj->instance ; my %read_models ; my %class_file_map ; for my $file (@files) { $logger->info("loading config file $file"); # now apply some translation to read model # - translate legacy warp parameters # - expand elements name my $tmp_model = Config::Model -> new( skip_include => 1, legacy => $legacy ) ; my @models = $tmp_model -> load ( 'Tmp' , $file ) ; my $rel_file = $file ; $rel_file =~ s/^$dir\/?//; die "wrong reg_exp" if $file eq $rel_file ; $class_file_map{$rel_file} = \@models ; # - move experience, description and level status into parameter info. foreach my $model_name (@models) { # no need to dclone model as Config::Model object is temporary my $new_model = $tmp_model -> get_model( $model_name ) ; foreach my $item (qw/description summary level experience status/) { foreach my $elt_name (keys %{$new_model->{element}}) { my $moved_data = delete $new_model->{$item}{$elt_name} ; next unless defined $moved_data ; $new_model->{element}{$elt_name}{$item} = $moved_data ; } delete $new_model->{$item} ; } # cleanup # Since the element are stored in a ordered hash, # load_data expects a array ref instead of a hash ref. # Build this array ref taking the element order into # account my $list = delete $new_model -> {element_list} ; my $elt_h = delete $new_model -> {element} ; $new_model -> {element} = [] ; map { push @{$new_model->{element}}, $_, $elt_h->{$_} } @$list ; # remove hash key with undefined values map { delete $new_model->{$_} unless defined $new_model->{$_} and $new_model->{$_} ne '' } keys %$new_model ; $read_models{$model_name} = $new_model ; } } # Create all classes listed in %read_models to avoid problems with # include statement while calling load_data my $class_element = $model_obj->fetch_element('class') ; map { $class_element->fetch_with_id($_) } keys %read_models ; #print Dumper \@read_models ; #require Tk::ObjScanner; Tk::ObjScanner::scan_object(\%read_models) ; $model_obj->instance->push_no_value_check(qw/store fetch type/) if $force_load; $logger->info("loading all extracted data in Config::Model::Itself"); $model_obj->load_data( {class => \%read_models} ) ; $model_obj->instance->pop_no_value_check() if $force_load; return $self->{map} = \%class_file_map ; } # internal sub get_perl_data_model{ my $self = shift ; my %args = @_ ; my $model_obj = $self->{model_object}; my $class_name = $args{class_name} || croak __PACKAGE__," read: undefined class name"; my $class_elt = $model_obj->fetch_element('class') ->fetch_with_id($class_name) ; my $model = $class_elt->dump_as_data ; # now apply some translation to read model # - Do NOT translate legacy warp parameters # - Do not compact elements name # - move experience, description and level status back in class info. # my $all_elt_data = $model->{element} || [] ; # for (my $i = 0 ; $i < @$all_elt_data; $i ++) { # my $elt_name = $all_elt_data->[$i++] ; # my $elt_data = $all_elt_data->[$i] ; # foreach my $item (qw/description/) { # my $moved_data = delete $elt_data->{$item} ; # next unless defined $moved_data ; # push @{$model->{$item}}, $elt_name, $moved_data ; # } # } # don't forget to add name $model->{name} = $class_name ; return $model ; } =head2 write_all ( model_dir => ... ) Will write back configuration model in the specified directory. The structure of the read directory is respected. =cut sub write_all { my $self = shift ; my %args = @_ ; my $model_obj = $self->{model_object} ; my $dir = $args{model_dir} || croak __PACKAGE__," write_all: undefined config dir"; my $map = $self->{map} ; unless (-d $dir ) { mkpath($dir,0, 0755) || die "Can't mkpath $dir:$!"; } my $i = $model_obj->instance ; # get list of all classes loaded by the editor my %loaded_classes = map { ($_ => 1); } $model_obj->fetch_element('class')->get_all_indexes ; # remove classes that are listed in map foreach my $file (keys %$map) { foreach my $class_name (@{$map->{$file}}) { delete $loaded_classes{$class_name} ; } } # add remaining classes in map my %new_map = map { my $f = $_; $f =~ s!::!/!g; ("$f.pl" => [ $_ ]) ; } keys %loaded_classes ; my %map_to_write = (%$map,%new_map) ; foreach my $file (keys %map_to_write) { $logger->info("writing config file $file"); my @data ; foreach my $class_name (@{$map_to_write{$file}}) { $logger->info("writing class $class_name"); my $model = $self-> get_perl_data_model(class_name => $class_name) ; push @data, $model ; # remove class name from above list delete $loaded_classes{$class_name} ; } my $wr_file = "$dir/$file" ; my $wr_dir = dirname($wr_file) ; unless (-d $wr_dir ) { mkpath($wr_dir,0, 0755) || die "Can't mkpath $wr_dir:$!"; } open (WR, ">$wr_file") || croak "Cannot open file $wr_file:$!" ; my $dumper = Data::Dumper->new([\@data]) ; $dumper->Terse(1) ; print WR $dumper->Dump , ";\n"; close WR ; } } =head2 list_class_element Returns a string listing all the class and elements. Useful for debugging your configuration model. =cut sub list_class_element { my $self = shift ; my $pad = shift || '' ; my $res = ''; my $meta_class = $self->{model_object}->fetch_element('class') ; foreach my $class_name ($meta_class->get_all_indexes ) { $res .= $self->list_one_class_element($class_name) ; } return $res ; } sub list_one_class_element { my $self = shift ; my $class_name = shift || return '' ; my $pad = shift || '' ; my $res = $pad."Class: $class_name\n"; my $meta_class = $self->{model_object}->fetch_element('class') -> fetch_with_id($class_name) ; my @elts = $meta_class->fetch_element('element')->get_all_indexes ; my @include = $meta_class->fetch_element('include')->fetch_all_values ; my $inc_after = $meta_class->grab_value('include_after') ; if (@include and not defined $inc_after) { map { $res .= $self->list_one_class_element($_,$pad.' ') ;} @include ; } return $res unless @elts ; foreach my $elt_name ( @elts) { my $type = $meta_class->grab_value("element:$elt_name type") ; $res .= $pad." - $elt_name ($type)\n"; if (@include and defined $inc_after and $inc_after eq $elt_name) { map { $res .=$self->list_one_class_element($_,$pad.' ') ;} @include ; } } return $res ; } =head2 get_dot_diagram Returns a graphviz dot file that represents the strcuture of the configuration model: =over =item * C are represented by solid lines =item * Class usage (i.e. C parameter) is represented by dashed lines. The name of the element is attached to the dashed line. =back =cut sub get_dot_diagram { my $self = shift ; my $dot = "digraph model {\n" ; my $meta_class = $self->{model_object}->fetch_element('class') ; foreach my $class_name ($meta_class->get_all_indexes ) { my $c_model = $self->{model_object}->config_model->get_raw_model($class_name); my $elts = $c_model->{element} || []; # array ref my $d_class = $class_name ; $d_class =~ s/::/__/g; my $elt_list = ''; my $use = ''; for (my $idx = 0; $idx < @$elts; $idx += 2) { my $elt_info = $elts->[$idx] ; my @elt_names = ref $elt_info ? @$elt_info : ($elt_info) ; my $type = $elts->[$idx+1]{type} ; foreach my $elt_name (@elt_names) { $elt_list .= "- $elt_name ($type)\\n"; $use .= $self->scan_used_class($d_class,$elt_name, $elts->[$idx+1]); } } $dot .= $d_class . qq! [shape=box label="$class_name\\n$elt_list"];\n! . $use . "\n"; my $include = $c_model->{include} ; if (defined $include) { my $inc_ref = ref $include ? $include : [ $include ] ; foreach my $t (@$inc_ref) { $t =~ s/::/__/g; $dot.= qq!$d_class -> $t ;\n!; } } } $dot .="}\n"; return $dot ; } sub scan_used_class { my ($self,$d_class,$elt_name,$ref) = @_ ; my $res = '' ; if (ref($ref) eq 'HASH') { foreach my $k (keys %$ref) { my $v = $ref->{$k} ; if ($k eq 'config_class_name') { $v =~ s/::/__/g; $res .= qq!$d_class -> $v ! . qq![ style=dashed, label="$elt_name" ];\n!; } if (ref $v) { $res .= $self->scan_used_class($d_class,$elt_name,$v); } } } elsif (ref($ref) eq 'ARRAY') { map {$res .= $self->scan_used_class($d_class,$elt_name,$_);} @$ref ; } return $res ; } 1; __END__ =head1 AUTHOR Dominique Dumont, (ddumont at cpan dot org) =head1 COPYRIGHT Copyright (C) 2007-2009 by Dominique Dumont =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the LGPL terms. =head1 SEE ALSO L, L, =cut