AcePerl

 view release on metacpan or  search on metacpan

Ace/Object.pm  view on Meta::CPAN

    }
    return $above || $left if $return_parent;
    return defined $pos ? $o->right($pos) : $o unless wantarray;
    return $o->col($pos);
}

### Flatten out part of the tree into an array ####
### along the row.  Will not follow object references.  ###
sub row {
  my $self = shift;
  my $pos = shift;
  my @r;
  my $o = defined $pos ? $self->right($pos) : $self;
  while (defined($o)) {
    push(@r,$o);
    $o = $o->right;
  }
  return @r;
}

### Flatten out part of the tree into an array ####
### along the column. Will not follow object references. ###
sub col {
  my $self = shift;
  my $pos = shift;
  $pos = 1 unless defined $pos;
  croak "Position must be positive" unless $pos >= 0;

  return ($self) unless $pos > 0;

  my @r;
  # This is for tag[1] semantics
  if ($pos == 1) {
    for (my $o=$self->right; defined($o); $o=$o->down) {
      push (@r,$o);
    }
  } else {
    # This is for tag[2] semantics
    for (my $o=$self->right; defined($o); $o=$o->down) {
      next unless defined(my $right = $o->right($pos-2));
      push (@r,$right->col);
    }
  }
  return @r;
}

#### Search for a tag, and return the column ####
#### Uses a breadth-first search (cols then rows) ####
sub search {
  my $self = shift;
  my $tag = shift unless $_[0]=~/^-/;
  my ($subtag,$pos,$filled) = rearrange(['SUBTAG','POS',['FILL','FILLED']],@_);
  my $lctag = lc $tag;

  # With caching, the old way of following ends up cloning the object
  # -- which we don't want.  So more-or-less emulate the earlier
  # behavior with an explicit get and fetch
  #  return $self->follow(-tag=>$tag,-filled=>$filled) if $filled;
  if ($filled) {
    my @node = $self->search($tag) or return;  # watch out for recursion!
    my @obj  = map {$_->fetch} @node;
    foreach (@obj) {$_->right if defined $_};  # trigger a fill
    return wantarray ? @obj : $obj[0];
  }

 TRY: {

    # look in our tag cache first
    if (exists $self->{'.PATHS'}) {

      # we've already cached the desired tree
      last TRY if exists $self->{'.PATHS'}{$lctag};
      
      # not cached, so try parents of tag
      my $m = $self->model;
      my @parents = $m->path($lctag) if $m;
      my $tree;
      foreach (@parents) {
	($tree = $self->{'.PATHS'}{lc $_}) && last;
      }
      if ($tree) {
	$self->{'.PATHS'}{$lctag} = $tree->search($tag);
	$self->_dirty(1);
	last TRY;
      }
    }

    # If the object hasn't been filled already, then we can use
    # acedb's query mechanism to fetch the subobject.  This is a
    # big win for large objects.  ...However, we have to disable
    # this feature if timestamps are active.
    unless ($self->filled) {
      my $subobject = $self->newFromText(
					 $self->db->show($self->class,$self->name,$tag),
					 $self->db
					);
      if ($subobject) {
	$subobject->{'.nocache'}++;
	$self->_attach_subtree($lctag => $subobject);
      } else {
	$self->{'.PATHS'}{$lctag} = undef;
      }
      $self->_dirty(1);
      last TRY;
    }
	
    my @col = $self->col;
    foreach (@col) {
      next unless $_->isTag;
      if (lc $_ eq $lctag) {
	$self->{'.PATHS'}{$lctag} = $_;
	$self->_dirty(1);
	last TRY;
      }
    }

    # if we get here, we didn't find it in the column,
    # so we call ourselves recursively to find it
    foreach (@col) {
      next unless $_->isTag;
      if (my $r = $_->search($tag)) {

Ace/Object.pm  view on Meta::CPAN

  # HACK! Some LongText entries may begin with newlines. This is within the Acedb spec.
  # Let's purge text entries of leading space and format them appropriate.
  # This should probably be handled in Freesubs.xs / Ace::split
  my $temp = $raw->[$start_row][$col];
#  if ($temp =~ /^\?txt\?\s*\n*/) {
#    $temp =~ s/^\?txt\?(\s*\\n*)/\?txt\?/;
#    $temp .= '?';
#  }
  my ($class,$name,$ts) = Ace->split($temp);

  my $self = $pack->new($class,$name,$db,!($start_row || $col));
  @{$self}{qw(.raw .start_row .end_row .col db)} = ($raw,$start_row,$end_row,$col,$db);
  $self->{'.timestamp'} = $ts if defined $ts;
  return $self;
}


# Return partial ace subtree at indicated tag
sub _at {
    my ($self,$tag) = @_;
    my $pos=0;

    # Removed a $` here to increase speed -- tim.cutts@incyte.com 2 Sep 1999

    if ($tag=~/(.*?)\[(\d+)\]$/) {
      $pos=$2;
      $tag=$1;
    }
    my $p;
    my $o = $self->right;
    while ($o) {
	return ($o->right($pos),$p,$self) if (lc($o) eq lc($tag));
	$p = $o;
	$o = $o->down;
    }
    return;
}


# Used to munge special data types.  Right now dates are the
# only examples.
sub _ace_format {
  my $self = shift;
  my ($class,$name) = @_;
  return undef unless defined $class && defined $name;
  return $class eq 'date' ? $self->_to_ace_date($name) : $name;
}

# It's an object unless it is one of these things
sub _isObject {
    return unless defined $_[0];
    $_[0] !~ /^(float|int|date|tag|txt|peptide|dna|scalar|[Tt]ext|comment)$/;
}

# utility routine used to split a tag path into individual components
# allows components to contain dots.
sub _split_tags {
  my $self = shift;
  my $tag = shift;
  $tag =~ s/\\\./$;/g; # protect backslashed dots
  return map { (my $x=$_)=~s/$;/./g; $x } split(/\./,$tag);
}


1;

__END__

=head1 NAME

Ace::Object - Manipulate  Ace Data Objects

=head1 SYNOPSIS

    # open database connection and get an object
    use Ace;
    $db = Ace->connect(-host => 'beta.crbm.cnrs-mop.fr',
                       -port => 20000100);
    $sequence  = $db->fetch(Sequence => 'D12345');
    
    # Inspect the object
    $r    = $sequence->at('Visible.Overlap_Right');
    @row  = $sequence->row;
    @col  = $sequence->col;
    @tags = $sequence->tags;
    
    # Explore object substructure
    @more_tags = $sequence->at('Visible')->tags;
    @col       = $sequence->at("Visible.$more_tags[1]")->col;

    # Follow a pointer into database
    $r     = $sequence->at('Visible.Overlap_Right')->fetch;
    $next  = $r->at('Visible.Overlap_left')->fetch;

    # Classy way to do the same thing
    $r     = $sequence->Overlap_right;
    $next  = $sequence->Overlap_left;

    # Pretty-print object
    print $sequence->asString;
    print $sequence->asTabs;
    print $sequence->asHTML;

    # Update object
    $sequence->replace('Visible.Overlap_Right',$r,'M55555');
    $sequence->add('Visible.Homology','GR91198');
    $sequence->delete('Source.Clone','MBR122');
    $sequence->commit();

    # Rollback changes
    $sequence->rollback()

    # Get errors
    print $sequence->error;

=head1 DESCRIPTION

I<Ace::Object> is the base class for objects returned from ACEDB
databases. Currently there is only one type of I<Ace::Object>, but
this may change in the future to support more interesting
object-specific behaviors.

Ace/Object.pm  view on Meta::CPAN


Also see B<col()> and B<get()>.

=head2 get() method

    $subtree    = $object->get($tag);
    @values     = $object->get($tag);
    @values     = $object->get($tag, $position);
    @values     = $object->get($tag => $subtag, $position);

The get() method will perform a breadth-first search through the
object (columns first, followed by rows) for the tag indicated by the
argument, returning the column of the portion of the subtree it points
to.  For example, this code fragment will return the value of the
"Fax" tag.

    ($fax_no) = $object->get('Fax');
         --> "33-67-521559"

The list versus scalar context semantics are the same as in at(), so
if you want to retrieve the scalar value pointed to by the indicated
tag, either use a list context as shown in the example, above, or a
dereference, as in:

     $fax_no = $object->get('Fax');
         --> "Fax"
     $fax_no = $object->get('Fax')->at;
         --> "33-67-521559"

An optional second argument to B<get()>, $position, allows you to
navigate the tree relative to the retrieved subtree.  Like the B<at()>
navigational indexes, $position must be a number greater than or equal
to zero.  In a scalar context, $position moves rightward through the
tree.  In an array context, $position implements "tag[2]" semantics.

For example:

     $fax_no = $object->get('Fax',0);
          --> "Fax"

     $fax_no = $object->get('Fax',1);
          --> "33-67-521559"

     $fax_no = $object->get('Fax',2);
          --> undef  # nothing beyond the fax number

     @address = $object->get('Address',2);
          --> ('CRBM duCNRS','BP 5051','34033 Montpellier','FRANCE',
               'mieg@kaa.cnrs-mop.fr,'33-67-613324','33-67-521559')

It is important to note that B<get()> only traverses tags.  It will
not traverse nodes that aren't tags, such as strings, integers or
objects.  This is in keeping with the behavior of the Ace query
language "show" command.

This restriction can lead to confusing results.  For example, consider
the following object:

 Clone: B0280  Position    Map            Sequence-III  Ends   Left   3569
                                                               Right  3585
                           Pmap           ctg377        -1040  -1024
               Positive    Positive_locus nhr-10
               Sequence    B0280
               Location    RW
               FingerPrint Gel_Number     0
                           Canonical_for  T20H1
                                          K10E5
                           Bands          1354          18


The following attempt to fetch the left and right positions of the
clone will fail, because the search for the "Left" and "Right" tags
cannot traverse "Sequence-III", which is an object, not a tag:

  my $left = $clone->get('Left');    # will NOT work
  my $right = $clone->get('Right');  # neither will this one

You must explicitly step over the non-tag node in order to make this
query work.  This syntax will work:

  my $left = $clone->get('Map',1)->get('Left');   # works
  my $left = $clone->get('Map',1)->get('Right');  # works

Or you might prefer to use the tag[2] syntax here:

  my($left,$right) = $clone->get('Map',1)->at('Ends[2]');

Although not frequently used, there is a form of get() which allows
you to stack subtags:

    $locus = $object->get('Positive'=>'Positive_locus');

Only on subtag is allowed.  You can follow this by a position if wish
to offset from the subtag.

    $locus = $object->get('Positive'=>'Positive_locus',1);

=head2 search() method

This is a deprecated synonym for get().

=head2 Autogenerated Access Methods

     $scalar = $object->Name_of_tag;
     $scalar = $object->Name_of_tag($position);
     @array  = $object->Name_of_tag;
     @array  = $object->Name_of_tag($position);
     @array  = $object->Name_of_tag($subtag=>$position);
     @array  = $object->Name_of_tag(-fill=>$tag);

The module attempts to autogenerate data access methods as needed.
For example, if you refer to a method named "Fax" (which doesn't
correspond to any of the built-in methods), then the code will call
the B<get()> method to find a tag named "Fax" and return its
contents.

Unlike get(), this method will B<always step into objects>.  This
means that:

   $map = $clone->Map;

will return the Sequence_Map object pointed to by the Clone's Map tag
and not simply a pointer to a portion of the Clone tree.  Therefore
autogenerated methods are functionally equivalent to the following:

   $map = $clone->get('Map')->fetch;

The scalar context semantics are also slightly different.  In a scalar
context, the autogenerated function will *always* move one step to the
right.

The list context semantics are identical to get().  If you want to
dereference all members of a multivalued tag, you have to do so manually:

  @papers = $author->Paper;
  foreach (@papers) { 
    my $paper = $_->fetch;
    print  $paper->asString;
  }

You can provide an optional positional index to rapidly navigate
through the tree or to obtain tag[2] behavior.  In the following
examples, the first two return the object's Fax number, and the third
returns all data two hops to the right of Address.

     $object   = $db->fetch(Author => 'Thierry-Mieg J');
     ($fax_no) = $object->Fax;
     $fax_no   = $object->Fax(1);
     @address  = $object->Address(2);

You may also position at a subtag, using this syntax:

     $representative = $object->Laboratory('Representative');

Both named tags and positions can be combined as follows:

     $lab_address = $object->Laboratory(Address=>2);

If you provide a -fill=>$tag argument, then the object fetch will
automatically fill the specified subtree, greatly improving
performance.  For example:

      $lab_address = $object->Laboratory(-filled=>'Address');

** NOTE: In a scalar context, if the node to the right of the tag is
** an object, the method will perform an implicit dereference of the
** object.  For example, in the case of:

    $lab = $author->Laboratory;

**NOTE: The object returned is the dereferenced Laboratory object, not
a node in the Author object.  You can control this by giving the
autogenerated method a numeric offset, such as Laboratory(0) or
Laboratory(1).  For backwards compatibility, Laboratory('@') is
equivalent to Laboratory(1).

The semantics of the autogenerated methods have changed subtly between
version 1.57 (the last stable release) and version 1.62.  In earlier
versions, calling an autogenerated method in a scalar context returned
the subtree rooted at the tag.  In the current version, an implicit
right() and dereference is performed.


=head2 fetch() method

    $new_object = $object->fetch;

Ace/Object.pm  view on Meta::CPAN


     my @col = $obj->col;
     my $cnt = scalar(@col);
     return ("$obj -- $cnt members",1);  # prune
            if $cnt > 10                 # if subtree to big

     # tags are bold
     return "<B>$obj</B>" if $obj->isTag;  

     # objects are blue
     return qq{<FONT COLOR="blue">$obj</FONT>} if $obj->isObject; 
   }

   $object->asHTML(\&process_cell);

=head2 asXML() method

   $result = $object->asXML;

asXML() returns a well-formed XML representation of the object.  The
particular representation is still under discussion, so this feature
is primarily for demonstration.

=head2 asGIF() method

  ($gif,$boxes) = $object->asGIF();
  ($gif,$boxes) = $object->asGIF(-clicks=>[[$x1,$y1],[$x2,$y2]...]
	                         -dimensions=> [$width,$height],
				 -coords    => [$top,$bottom],
				 -display   => $display_type,
				 -view      => $view_type,
				 -getcoords => $true_or_false
	                         );

asGIF() returns the object as a GIF image.  The contents of the GIF
will be whatever xace would ordinarily display in graphics mode, and
will vary for different object classes.

You can optionally provide asGIF with a B<-clicks> argument to
simulate the action of a user clicking on the image.  The click
coordinates should be formatted as an array reference that contains a
series of two-element subarrays, each corresponding to the X and Y
coordinates of a single mouse click.  There is currently no way to
pass information about middle or right mouse clicks, dragging
operations, or keystrokes.  You may also specify a B<-dimensions> to
control the width and height of the returned GIF.  Since there is no
way of obtaining the preferred size of the image in advance, this is
not usually useful.

The optional B<-display> argument allows you to specify an alternate
display for the object.  For example, Clones can be displayed either
with the PMAP display or with the TREE display.  If not specified, the
default display is used.

The optional B<-view> argument allows you to specify an alternative
view for MAP objects only.  If not specified, you'll get the default
view.

The option B<-coords> argument allows you to provide the top and
bottom of the display for MAP objects only.  These coordinates are in
the map's native coordinate system (cM, bp).  By default, AceDB will
show most (but not necessarily all) of the map according to xace's
display rules.  If you call this method with the B<-getcoords>
argument and a true value, it will return a two-element array
containing the coordinates of the top and bottom of the map.

asGIF() returns a two-element array.  The first element is the GIF
data.  The second element is an array reference that indicates special 
areas of the image called "boxes."  Boxes are rectangular areas that
surround buttons, and certain displayed objects.  Using the contents
of the boxes array, you can turn the GIF image into a client-side
image map.  Unfortunately, not everything that is clickable is
represented as a box.  You still have to pass clicks on unknown image
areas back to the server for processing.

Each box in the array is a hash reference containing the following
keys:

    'coordinates'  => [$left,$top,$right,$bottom]
    'class'        => object class or "BUTTON"
    'name'         => object name, if any
    'comment'      => a text comment of some sort

I<coordinates> points to an array of points indicating the top-left and 
bottom-right corners of the rectangle.  I<class> indicates the class
of the object this rectangle surrounds.  It may be a database object,
or the special word "BUTTON" for one of the display action buttons.
I<name> indicates the name of the object or the button.  I<comment> is 
some piece of information about the object in question.  You can
display it in the status bar of the browser or in a popup window if
your browser provides that facility.

=head2 asDNA() and asPeptide() methods

    $dna = $object->asDNA();
    $peptide = $object->asPeptide();

If you are dealing with a sequence object of some sort, these methods
will return strings corresponding to the DNA or peptide sequence in
FASTA format.

=head2 add_row() method

    $result_code = $object->add_row($tag=>$value);    
    $result_code = $object->add_row($tag=>[list,of,values]);    
    $result_code = $object->add(-path=>$tag,
				-value=>$value);

add_row() updates the tree by adding data to the indicated tag path.  The
example given below adds the value "555-1212" to a new Address entry
named "Pager".  You may call add_row() a second time to add a new value
under this tag, creating multi-valued entries.

 $object->add_row('Address.Pager'=>'555-1212');

You may provide a list of values to add an entire row of data.  For
example:

 $sequence->add_row('Assembly_tags'=>['Finished Left',38949,38952,'AC3']);

Actually, the array reference is not entirely necessary, and if you
prefer you can use this more concise notation:

 $sequence->add_row('Assembly_tags','Finished Left',38949,38952,'AC3');

No check is done against the database model for the correct data type
or tag path.  The update isn't actually performed until you call
commit(), at which time a result code indicates whether the database
update was successful.

You may create objects that reference other objects this way:

Ace/Object.pm  view on Meta::CPAN

    return unless defined($self->right);
    my $string = "<TABLE BORDER>\n<TR ALIGN=LEFT VALIGN=TOP><TH>$self</TH>";
    $modify_code = \&_default_makeHTML unless $modify_code;
    $self->right->_asHTML(\$string,1,2,$modify_code);
    $string .= "</TR>\n</TABLE>\n";
    return $string;
}

### Get the FASTA-format DNA/Peptide representation for this object ###
### (if appropriate) ###
sub asDNA {
  return shift()->_special_dump('dna');
}

sub asPeptide {
  return shift()->_special_dump('peptide');
}

sub _special_dump {
  my $self = shift;
  my $dump_format = shift;
  return unless $self->db->count($self->class,$self->name);
  my $result = $self->db->raw_query($dump_format);
  $result =~ s!^//.*!!ms;
  $result;
}

#### As tab-delimited table ####
sub asTable {
    my $self = shift;
    my $string = "$self\t";
    my $right = $self->right;
    $right->_asTable(\$string,1,2) if defined($right);
    return $string . "\n";
}

#### In "ace" format ####
sub asAce {
  my $self = shift;
  my $string = $self->isRoot ? join(' ',$self->class,':',$self->escape) . "\n" : '';
  $self->right->_asAce(\$string,0,[]);
  return "$string\n\n";
}

### Pretty-printed version ###
sub asString {
  my $self = shift;
  my $MAXWIDTH = shift || $DEFAULT_WIDTH;
  my $tabs = $self->asTable;
  return "$self" unless $tabs;
  my(@lines) = split("\n",$tabs);
  my($result,@max);
  foreach (@lines) {
    my(@fields) = split("\t");
    for (my $i=0;$i<@fields;$i++) {
      $max[$i] = length($fields[$i]) if
	!defined($max[$i]) or $max[$i] < length($fields[$i]);
    }
  }
  foreach (@max) { $_ = $MAXWIDTH if $_ > $MAXWIDTH; } # crunch long lines
  my $format1 = join(' ',map { "^"."<"x $max[$_] } (0..$#max)) . "\n";
  my $format2 =   ' ' . join('  ',map { "^"."<"x ($max[$_]-1) } (0..$#max)) . "~~\n";
  $^A = '';
  foreach (@lines) {
    my @data = split("\t");
    push(@data,('')x(@max-@data));
    formline ($format1,@data);
    formline ($format2,@data);
  }
  return ($result = $^A,$^A='')[0];
}

# run a series of GIF commands and return the Gif and the semi-parsed
# "boxes" structure.  Commands is typically a series of mouseclicks
# ($gif,$boxes) = $aceObject->asGif(-clicks=>[[$x1,$y1],[$x2,$y2]...],
#                                   -dimensions=>[$x,$y]);
sub asGif {
  my $self = shift;
  my ($clicks,$dimensions,$display,$view,$coords,$getcoords) = rearrange(['CLICKS',
									  ['DIMENSIONS','DIM'],
									  'DISPLAY',
									  'VIEW',
									  'COORDS',
									  'GETCOORDS',
									  ],@_);
  $display = "-D $display" if $display;
  $view    = "-view $view" if $view;
  my $c;
  if ($coords) {
    $c    =  ref($coords) ? "-coords @$coords" : "-coords $coords";
  }
  my @commands;
  if ($view || $c || $self->class =~ /Map/i) {
      @commands = "gif map \"@{[$self->name]}\" $view $c";
  } else {
      @commands = "gif display $display $view @{[$self->class]} \"@{[$self->name]}\"";
  }
  push(@commands,"Dimensions @$dimensions") if ref($dimensions);
  push(@commands,map { "mouseclick @{$_}" } @$clicks) if ref($clicks);

  if ($getcoords) { # just want the coordinates
    my ($start,$stop);
    my $data = $self->db->raw_query(join(' ; ',@commands));    
    return unless $data =~ /\"[^\"]+\" ([\d.-]+) ([\d.-]+)/;
    ($start,$stop) = ($1,$2);
    return ($start,$stop);
  }

  push(@commands,"gifdump -");

  # do the query
  my $data = $self->db->raw_query(join(' ; ',@commands));

  # A $' has been removed here to improve speed -- tim.cutts@incyte.com 2 Sep 1999

  # did this query succeed?
  my ($bytes, $trim);
  return unless ($bytes, $trim) = $data=~m!^// (\d+) bytes\n\0*(.+)!sm;

  my $gif = substr($trim,0,$bytes);

  # now process the boxes
  my @b;
  my @boxes = split("\n",substr($trim,$bytes));
  foreach (@boxes) {
    last if m!^//!;
    chomp;
    my ($left,$top,$right,$bottom,$class,$name,$comments) = 
      m/^\s*\d*\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\w+):"(.+)"\s*(.*)/;
    next unless defined $left;
    $comments=~s/\s+$//; # sometimes there's extra white space at the end
    my $box = {'coordinates'=>[$left,$top,$right,$bottom],
	       'class'=>$class,
	       'name' =>$name,
	       'comment'=>$comments};
    push (@b,$box);
  }
  return ($gif,\@b);
}

############## timestamp and comment information ############
sub timestamp {
    my $self = shift;
    return $self->{'.timestamp'} = $_[0] if defined $_[0];
    if ($self->db && !$self->{'.timestamp'}) {
      $self->_fill;
      $self->_parse;
    }
    return $self->{'.timestamp'} if $self->{'.timestamp'};
    return unless defined $self->right;
    return $self->{'.timestamp'} = $self->right->timestamp;
}

sub comment {
    my $self = shift;
    return $self->{'.comment'} = $_[0] if defined $_[0];
    if ($self->db && !$self->{'.comment'}) {
      $self->_fill;
      $self->_parse;
    }
    return $self->{'.comment'};
}

### Return list of all the tags in the object ###
sub tags {
    my $self = shift;
    my $current = $self->right;
    my @tags;
    while (defined($current)) {
	push(@tags,$current);
	$current = $current->down;
    }
    return @tags;
}

################# kill an object ################
# Removes the object from the database immediately.
sub kill {
  my $self = shift;
  return unless my $db = $self->db;
  return 1 unless $db->count($self->class,$self->name);
  my $result = $db->raw_query("kill");
  if (defined($result) and $result=~/write access/im) {  # this keeps changing
    $Ace::Error = "Write access denied";
    return;
  }
  # uncache cached values and clear the object out
  # as best we can
  delete @{$self}{qw[.PATHS .right .raw .down]};
  1;
}

# sub isTimestamp {
#   my $self = shift;
#   return 1 if $self->class eq 'UserSession';
#   return;
# }

sub isComment {
  my $self = shift;
  return 1 if $self->class eq 'Comment';
  return;
}

################# add a new row #############
#  Only changes local copy until you perform commit() #
#  returns true if this is a valid thing to do #
sub add_row {
  my $self = shift;
  my($tag,@newvalue) = rearrange([['TAG','PATH'],'VALUE'],@_);

  # flatten array refs into array
  my @values = map { ref($_) && ref($_) eq 'ARRAY' ? @$_ : $_ } @newvalue;

  # make sure that this entry doesn't already exist
  unless ($tag =~ /\./) {
    my $model = $self->model;
    my @intermediate_tags = $model->path($tag);
    $tag = join '.',@intermediate_tags,$tag;
  }
  my $row = join(".",($tag,map { (my $x = $_) =~s/\./\\./g; $x } @values));
  return if $self->at($row);  # an identical row already exists in the object

  # If we get here then we need to turn @values into an array of Ace::Objects
  # for insertion.  Also need to link them together into a row.
  my $previous;
  foreach (@values) {
    if (ref($_) && $_->isa('Ace::Object')) {
      $_ = $_->_clone;
    } else {
      $_ = $self->new('scalar',$_);
    }
    $previous->{'.right'} = $_ if defined $previous;
    $previous = $_;
    $_->{'.right'} = undef; # make sure it doesn't automatically expand!
  }

  # position at the indicated tag (creating it if necessary)
  my (@tags) = $self->_split_tags($tag);
  my $p = $self;
  foreach (@tags) {
    $p = $p->_insert($_);
  }
  if ($p->{'.right'}) {
    $p = $p->{'.right'};
    while (1) { 
      last unless $p->{'.down'};
      $p = $p->{'.down'};
    }
    $p->{'.down'} = $values[0];
  } else {
    $p->{'.right'} = $values[0];
  }

  push(@{$self->{'.update'}},join(' ',map { Ace->freeprotect($_) } (@tags,@values)));
  delete $self->{'.PATHS'}; # uncache cached values
  $self->_dirty(1);
  1;
}

# Use this method to add an entire subobject to the right of the tag.
# The tree may come from another database.
sub add_tree {
  my $self = shift;
  my($tag,$value,@rest) = rearrange([['TAG','PATH'],['VALUE','TREE']],@_);
  croak "Value must be an Ace::Object" unless ref($value) && $value->isa('Ace::Object');

  unless ($tag =~ /\./) {
    my $model = $self->model;
    my @intermediate_tags = $model->path($tag);
    $tag = join '.',@intermediate_tags,$tag;
  }

  # position at the indicated tag, creating it if necessary
  my (@tags) = $self->_split_tags($tag);
  my $p = $self;
  foreach (@tags) {
    $p = $p->_insert($_);
  }
  # Copy the subtree too
  if ($p->{'.right'}) {
    $p = $p->{'.right'};
    while (1) { 
      last unless $p->{'.down'};
      $p = $p->{'.down'};
    }
    $p->{'.down'} = $value->{'.right'};
  } else {
    $p->{'.right'} = $value->{'.right'};
  }
  push(@{$self->{'.update'}},map { join(' ',@tags,$_) } split("\n",$value->asAce));
  delete $self->{'.PATHS'}; # uncache cached values
  $self->_dirty(1);
  1;
}

################# delete a portion of the tree #############
# Only changes local copy until you perform commit() #
#  returns true if this is a valid thing to do.
sub delete {
  my $self = shift;
  my($tag,$oldvalue,@rest) = rearrange([['TAG','PATH'],['VALUE','OLDVALUE','OLD']],@_);

  # flatten array refs into array
  my @values;
  @values = map { ref($_) && ref($_) eq 'ARRAY' ? @$_ : $_ } ($oldvalue,@rest) 
    if defined($oldvalue);

  unless ($tag =~ /\./) {
    my $model = $self->model;
    my @intermediate_tags = $model->path($tag);
    $tag = join '.',@intermediate_tags,$tag;
  }

  my $row = join(".",($tag,map { (my $x = $_) =~s/\./\\./g; $x } @values));
  my $subtree = $self->at($row,undef,1);  # returns the parent

  if (@values
      && defined($subtree->{'.right'})
      && "$subtree->{'.right'}" eq $oldvalue) {
    $subtree->{'.right'} = $subtree->{'.right'}->down;
  } else {
    $subtree->{'.down'} = $subtree->{'.down'}->{'.down'}
  }

  push(@{$self->{'.update'}},join(' ','-D',
				 map { Ace->freeprotect($_) } ($self->_split_tags($tag),@values)));
  delete $self->{'.PATHS'}; # uncache cached values
  $self->_dirty(0);
  $self->db->file_cache_delete($self);
  1;
}


################# delete a portion of the tree #############
# Only changes local copy until you perform commit() #
#  returns true if this is a valid thing to do #
sub replace {
  my $self = shift;
  my($tag,$oldvalue,$newvalue,@rest) = rearrange([['TAG','PATH'],
						  ['OLDVALUE','OLD'],
						  ['NEWVALUE','NEW']],@_);
    $self->delete($tag,$oldvalue);
    $self->add($tag,$newvalue,@rest);
    delete $self->{'.PATHS'}; # uncache cached values
    1;
}

# commit changes from local copy to database copy
sub commit {
  my $self = shift;
  return unless my $db = $self->db;
  
  my ($retval,@cmd);
  my $name = $self->{'name'};
  return unless defined $name;
  
  $name =~ s/([^a-zA-Z0-9_-])/\\$1/g;
  return 1 unless exists $self->{'.update'} && $self->{'.update'};

  $Ace::Error = '';
  my $result = '';

  # bad design alert: the following breaks encapsulation
  if ($db->db->can('write')) { # new way for socket server
    my $cmd = join "\n","$self->{'class'} : $name",@{$self->{'.update'}};
    warn $cmd if $self->debug;
    $result = $db->raw_query($cmd,0,'parse');  # sets Ace::Error for us
  } else {   # old way for RPC server and local
    my $cmd = join('; ',"$self->{'class'} : $name",
		   @{$self->{'.update'}});
    warn $cmd if $self->debug;
    $result = $db->raw_query("parse = $cmd");
  }

  if (defined($result) and $result=~/write( or admin)? access/im) {  # this keeps changing
    $Ace::Error = "Write access denied";
  } elsif (defined($result) and $result =~ /sorry|parse error/mi) {
    $Ace::Error = $result;
  }
  return if $Ace::Error;
  undef $self->{'.update'};
  # this will force a fresh retrieval of the object
  # and synchronize our in-memory copy with the db
  delete $self->{'.right'};
  delete $self->{'.PATHS'};
  return 1;
}

# undo changes
sub rollback {
    my $self = shift;
    undef $self->{'.update'};
    # this will force object to be reloaded from database
    # next time it is needed.
    delete $self->{'.right'};
    delete $self->{'.PATHS'};
    1;
}

sub debug {
    my $self = shift;
    Ace->debug(@_);
}

### Get or set the date style (actually calls through to the database object) ###
sub date_style {
  my $self = shift;
  return unless $self->db;
  return $self->db->date_style(@_);
}

sub _asHTML {
  my($self,$out,$position,$level,$morph_code) = @_;
  do {
    $$out .= "<TR ALIGN=LEFT VALIGN=TOP>" unless $position;
    $$out .= "<TD></TD>" x ($level-$position-1);
    my ($cell,$prune,$did_it_myself) = $morph_code->($self);
    $$out .= $did_it_myself ? $cell : "<TD>$cell</TD>";
    if ($self->comment) {
      my ($cell,$p,$d) = $morph_code->($self->comment);
      $$out .= $d ? $cell : "<TD>$cell</TD>";
      $$out .= "</TR>\n" . "<TD></TD>" x $level unless $self->down && !defined($self->right);
    }
    $level = $self->right->_asHTML($out,$level,$level+1,$morph_code) if defined($self->right) && !$prune;
    $$out .= "</TR>\n" if defined($self = $self->down);
    $position = 0;
  } while defined $self;
  return --$level;
}


# This function is overly long because it is optimized to prevent parsing
# parts of the tree that haven't previously been parsed.
sub _asTable {
    my($self,$out,$position,$level) = @_;
    do {
      if ($self->{'.raw'}) {  # we still have raw data, so we can optimize
	my ($a,$start,$end) = @{$self}{ qw(.col .start_row .end_row) };
	my @to_append = map { join("\t",@{$_}[$a..$#{$_}]) } @{$self->{'.raw'}}[$start..$end];
	my $new_row;
	foreach (@to_append) {
	  # hack alert
	  s/(\?.*?[^\\]\?.*?[^\\]\?)\S*/$self->_ace_format(Ace->split($1))/eg;
	  if ($new_row++) {
	    $$out .= "\n";
	    $$out .= "\t" x ($level-1) 
	  }
	  $$out .= $_;
	}
	return $level-1;
      }

      $$out .= "\t" x ($level-$position-1);
      $$out .= $self->name . "\t";
      if ($self->comment) {
	$$out .= $self->comment;
	$$out .= "\n" . "\t" x $level unless $self->down && !defined($self->right);
      }
      $level = $self->right->_asTable($out,$level,$level+1)
	if defined $self->right;
      $$out .= "\n" if defined($self = $self->down);
      $position = 0;
    } while defined $self;
    return --$level;
}

# This is the default code that will be called during construction of
# the HTML table.  It returns a two-member list consisting of the modified
# entry and (optionally) a true value if we are to prune here.  The returned string
# will be placed inside a <TD></TD> tag.  There's nothing you can do about that.
sub _default_makeHTML {
  my $self = shift;
  my ($string,$prune) = ("$self",0);
  return ($string,$prune) unless $self->isObject || $self->isTag;

  if ($self->isTag) {
    $string = "<B>$self</B>";
  } elsif ($self->isComment) {
    $string = "<I>$self</I>";
  }  else {
    $string = qq{<FONT COLOR="blue">$self</FONT>} ;
  }
  return ($string,$prune);
}

# Insert a new tag or value.
# Local only. Will not affect the database.
# Returns the inserted tag, or the preexisting
# tag, if already there.
sub _insert {
    my ($self,$tag) = @_;
    my $p = $self->{'.right'};
    return $self->{'.right'} = $self->new('tag',$tag)
	unless $p;
    while ($p) {
	return $p if "$p" eq $tag;
	last unless $p->{'.down'};
	$p = $p->{'.down'};
    }



( run in 0.524 second using v1.01-cache-2.11-cpan-140bd7fdf52 )