lib/catissue/database.rb
def save_with_template(obj, template)
      # special cases to work around caTissue bugs

      # Protocol create doesn't add the coordinators to the save template.
      # TODO - find out why
      if CaTissue::CollectionProtocol === obj and obj.identifier.nil? and template.coordinators.empty? and not obj.coordinators.empty? then
        ensure_exists(obj.coordinators)
        template.coordinators.merge(obj.coordinators)
      elsif CaTissue::CollectionProtocolRegistration === obj  and template.collection_protocol then
        template.collection_protocol.collection_protocol_events.clear
      elsif CaTissue::Specimen === obj then
        if obj.position and obj.position.identifier then
          add_position_to_specimen_template(obj, template)
        end
        # Anticipate the caTissue disposed Specimen update side-effect by removing
        # the consent tier statuses.
        if obj.disposed? then
          unless obj.consent_tier_statuses.empty? then
            obj.consent_tier_statuses.clear
            template.consent_tier_statuses.clear
            logger.debug { "Anticipated a caTissue side-effect by clearing the disposed #{obj.qp} consent tier statuses prior to save." }
          end
        end
      # TODO - is there a test case for this? Isn't EID create delegated to
      # specimen create, which cascades to the EID?
      # elsif obj.identifier.nil? and CaTissue::ExternalIdentifier === obj then
      #   # application service save
      #   result = submit_save_template(obj, template)
      #   # if app service is not broken, then sync the result and return
      #   if obj.identifier then
      #     sync_saved(obj, result)
      #     return
      #   end
      #   logger.debug { "Work around caTissue ExternalIdentifier create bug by updating the phantom caTissue auto-generated empty #{obj.specimen} EID directly with SQL..." }
      #   # app service is broken; fetch the identifier and set directly via SQL
      #   tmpl = obj.class.new
      #   tmpl.setSpecimen(obj.specimen)
      #   eids = query(tmpl).select { |eid| eid.name.nil? }
      #   if eids.size > 1 then
      #     raise DatabaseError.new("#{spc} has more than external identifier without a name: #{eids}")
      #   end
      #   # Set the identifier.
      #   obj.identifier = eids.first.identifier
      #   # Call the SQL
      #   @executor.transact(UPD_EID_SQL, obj.name, obj.value, obj.specimen.identifier, obj.identifier)
      #   logger.debug { "caTissue #{obj} create work-around completed." }
      #   return
      elsif obj.identifier and CaTissue::SpecimenEventParameters === obj then
        # TODO - this case occurs in the simple_test migration; fix it there and remove this check.
        # TODO - KLUDGE!!!! FIX AT SOURCE AND REMOVE SEP KLUDGE AS WELL!!!!
        # Fix this with an auto-gen add_defaults?
        if template.user.nil? then template.user = query(template, :user).first end
        if template.specimen.nil? and template.specimen_collection_group.nil? then
           template.specimen = query(template, :specimen).first
           template.specimen_collection_group = query(template, :specimen_collection_group).first
        end
      elsif obj.identifier and CaTissue::SpecimenCollectionGroup === obj then
        # add the extraneous SCG template CPR protocol and PPI, if necessary 
        cpr = obj.collection_protocol_registration
        if cpr.nil? then raise Jinx::ValidationError.new("#{obj} cannot be updated since it is missing a CPR") end
        tcpr = template.collection_protocol_registration
        if tcpr.nil? then raise Jinx::ValidationError.new("#{obj} CPR #{cpr} was not copied to the update template #{tcpr}") end
        if tcpr.collection_protocol.nil? then
          pcl = lazy_loader.enable { cpr.collection_protocol }
          if pcl.nil? then raise Jinx::ValidationError.new("#{obj} cannot be updated since it is missing a referenced CPR #{cpr} protocol") end
          tpcl = pcl.copy(:identifier)
          logger.debug { "Work around caTissue bug by adding extraneous #{template} #{tcpr} protocol #{tpcl}..." }
          tmpl.collection_protocol = tpcl
        end
        if tcpr.protocol_participant_identifier.nil? then
          ppi = lazy_loader.enable { cpr.protocol_participant_identifier }
          if ppi.nil? then
            raise Jinx::ValidationError.new("#{obj} cannot be updated since it is missing a referenced CPR #{cpr} PPI required to work around a caTissue SCG update bug")
          end
          tppi = ppi.copy(:identifier)
          logger.debug { "Work around caTissue bug by adding extraneous #{template} #{tcpr} PPI #{tppi}..." }
          tmpl.protocol_participant_identifier = tppi
        end
        unless obj.received? then
          rep = obj.instance_eval { create_default_received_event_parameters }
          if rep.nil? then raise CaRuby::DatabaseError.new("Default received event parameters were not added to #{obj}.") end
          rep.copy.merge_attributes(:user => rep.user, :specimen_collection_group => template)
        end
        unless obj.collected? then
          cep = obj.instance_eval { create_default_collection_event_parameters }
          if cep.nil? then raise CaRuby::DatabaseError.new("Default collection event parameters were not added to #{obj}.") end
          cep.copy.merge_attributes(:user => cep.user, :specimen_collection_group => template)
        end
      elsif Annotation::Proxy === obj then
        raise CaRuby::DatabaseError.new("Annotation proxy direct database save is not supported: #{obj}")
      elsif Annotation === obj and obj.class.primary? then
        copy_annotation_proxy_owner_to_template(obj, template)
      end

      # Work around a caTissue bug by removing CollectibleEventParameters.
      ceps = strip_collectible_event_parameters(obj, template) if Collectible === obj
      
      # delegate to the standard save
      super

      # post-process the deferred CEPs
      if ceps and not ceps.empty? then
        # the owner => target CEP hash
        hash = LazyHash.new { Array.new }
        ceps.each { |cep| hash[cep.owner] << cep }
        hash.each do |owner, teps|
          logger.debug { "Refetch the #{owner} event parameters to work around a caTissue bug..." }
          fetched = fetch_association(owner, :specimen_event_parameters)
          teps.each do |tep|
            match = fetched.detect { |fep| tep.class === fep }
            if match then
              logger.debug { "Matched the #{owner} event parameter #{tep} to the fetched #{fep}." }
              tep.merge(fep)
            else
              logger.debug { "#{owner} event parameter #{tep} does not match a fetched event parameters object." }
            end
          end
        end
      end
    end
def save_with_template(obj, template)
      # special cases to work around caTissue bugs

      # Protocol create doesn't add the coordinators to the save template.
      # TODO - find out why
      if CaTissue::CollectionProtocol === obj and obj.identifier.nil? and template.coordinators.empty? and not obj.coordinators.empty? then
        ensure_exists(obj.coordinators)
        template.coordinators.merge(obj.coordinators)
      elsif CaTissue::CollectionProtocolRegistration === obj  and template.collection_protocol then
        template.collection_protocol.collection_protocol_events.clear
      elsif CaTissue::Specimen === obj then
        if obj.position and obj.position.identifier then
          add_position_to_specimen_template(obj, template)
        end
        # Anticipate the caTissue disposed Specimen update side-effect by removing
        # the consent tier statuses.
        if obj.disposed? then
          unless obj.consent_tier_statuses.empty? then
            obj.consent_tier_statuses.clear
            template.consent_tier_statuses.clear
            logger.debug { "Anticipated a caTissue side-effect by clearing the disposed #{obj.qp} consent tier statuses prior to save." }
          end
        end
      # TODO - is there a test case for this? Isn't EID create delegated to
      # specimen create, which cascades to the EID?
      # elsif obj.identifier.nil? and CaTissue::ExternalIdentifier === obj then
      #   # application service save
      #   result = submit_save_template(obj, template)
      #   # if app service is not broken, then sync the result and return
      #   if obj.identifier then
      #     sync_saved(obj, result)
      #     return
      #   end
      #   logger.debug { "Work around caTissue ExternalIdentifier create bug by updating the phantom caTissue auto-generated empty #{obj.specimen} EID directly with SQL..." }
      #   # app service is broken; fetch the identifier and set directly via SQL
      #   tmpl = obj.class.new
      #   tmpl.setSpecimen(obj.specimen)
      #   eids = query(tmpl).select { |eid| eid.name.nil? }
      #   if eids.size > 1 then
      #     raise DatabaseError.new("#{spc} has more than external identifier without a name: #{eids}")
      #   end
      #   # Set the identifier.
      #   obj.identifier = eids.first.identifier
      #   # Call the SQL
      #   @executor.transact(UPD_EID_SQL, obj.name, obj.value, obj.specimen.identifier, obj.identifier)
      #   logger.debug { "caTissue #{obj} create work-around completed." }
      #   return
      elsif obj.identifier and CaTissue::SpecimenEventParameters === obj then
        # TODO - this case occurs in the simple_test migration; fix it there and remove this check.
        # TODO - KLUDGE!!!! FIX AT SOURCE AND REMOVE SEP KLUDGE AS WELL!!!!
        # Fix this with an auto-gen add_defaults?
        if template.user.nil? then template.user = query(template, :user).first end
        if template.specimen.nil? and template.specimen_collection_group.nil? then
           template.specimen = query(template, :specimen).first
           template.specimen_collection_group = query(template, :specimen_collection_group).first
        end
      elsif obj.identifier and CaTissue::SpecimenCollectionGroup === obj then
        # add the extraneous SCG template CPR protocol and PPI, if necessary 
        cpr = obj.collection_protocol_registration
        if cpr.nil? then raise Jinx::ValidationError.new("#{obj} cannot be updated since it is missing a CPR") end
        tcpr = template.collection_protocol_registration
        if tcpr.nil? then raise Jinx::ValidationError.new("#{obj} CPR #{cpr} was not copied to the update template #{tcpr}") end
        if tcpr.collection_protocol.nil? then
          pcl = lazy_loader.enable { cpr.collection_protocol }
          if pcl.nil? then raise Jinx::ValidationError.new("#{obj} cannot be updated since it is missing a referenced CPR #{cpr} protocol") end
          tpcl = pcl.copy(:identifier)
          logger.debug { "Work around caTissue bug by adding extraneous #{template} #{tcpr} protocol #{tpcl}..." }
          tmpl.collection_protocol = tpcl
        end
        if tcpr.protocol_participant_identifier.nil? then
          ppi = lazy_loader.enable { cpr.protocol_participant_identifier }
          if ppi.nil? then
            raise Jinx::ValidationError.new("#{obj} cannot be updated since it is missing a referenced CPR #{cpr} PPI required to work around a caTissue SCG update bug")
          end
          tppi = ppi.copy(:identifier)
          logger.debug { "Work around caTissue bug by adding extraneous #{template} #{tcpr} PPI #{tppi}..." }
          tmpl.protocol_participant_identifier = tppi
        end
        unless obj.received? then
          rep = obj.instance_eval { create_default_received_event_parameters }
          if rep.nil? then raise CaRuby::DatabaseError.new("Default received event parameters were not added to #{obj}.") end
          rep.copy.merge_attributes(:user => rep.user, :specimen_collection_group => template)
        end
        unless obj.collected? then
          cep = obj.instance_eval { create_default_collection_event_parameters }
          if cep.nil? then raise CaRuby::DatabaseError.new("Default collection event parameters were not added to #{obj}.") end
          cep.copy.merge_attributes(:user => cep.user, :specimen_collection_group => template)
        end
      elsif Annotation::Proxy === obj then
        raise CaRuby::DatabaseError.new("Annotation proxy direct database save is not supported: #{obj}")
      elsif Annotation === obj and obj.class.primary? then
        copy_annotation_proxy_owner_to_template(obj, template)
      end

      # Work around a caTissue bug by removing CollectibleEventParameters.
      ceps = strip_collectible_event_parameters(obj, template) if Collectible === obj
      
      # delegate to the standard save
      super

      # post-process the deferred CEPs
      if ceps and not ceps.empty? then
        # the owner => target CEP hash
        hash = LazyHash.new { Array.new }
        ceps.each { |cep| hash[cep.owner] << cep }
        hash.each do |owner, teps|
          logger.debug { "Refetch the #{owner} event parameters to work around a caTissue bug..." }
          fetched = fetch_association(owner, :specimen_event_parameters)
          teps.each do |tep|
            match = fetched.detect { |fep| tep.class === fep }
            if match then
              logger.debug { "Matched the #{owner} event parameter #{tep} to the fetched #{fep}." }
              tep.merge(fep)
            else
              logger.debug { "#{owner} event parameter #{tep} does not match a fetched event parameters object." }
            end
          end
        end
      end
    end
Augments +CaRuby::Database.save_with_template+ to work around the following caTissue anomalies:

 @quirk caTissue Bug #149: API update TissueSpecimen position validation incorrect.
   The Specimen update argument must reference the old position, even though the position is not
   updatable, unless old status is Pending. The validation defect described in Bug #149 requires
   a work-around that is also used for a different reason described in the following paragraph.

 @quirk caTissue Update of a {CaTissue::Specimen} which references a position must include the former
   position in the caTissue service update argument. A Specimen position is altered as a side-effect
   by creating a proxy save {CaTissue::TransferEventParameters}. The changed position is not reflected
   in the Specimen position, which must be refetched to reflect the database state. This fetch is
   done automatically by +CaRuby::Database+ as part of the save proxy mechanism. The Specimen update
   template must include a reference to the former position but not the changed position.

   However, the Specimen +CaRuby::Writer.update+ argument will include the changed position, not the
   former position. The template built by +CaRuby::Writer.update+ for submission to the caTissue app
   does not include a position reference, since the position has a save proxy which handles position
   change as part of the +CaRuby::Writer+ update dependent propagation.

   Thus, updating a Specimen which includes a position change is performed as follows:
   * reconstitute the former position from the Position snapshot taken as part of the
      +CaRuby::Persistable+ change tracker.
   * add the former position to the template (which will now differ from the +CaRuby::Writer.update+
     argument).
   * submit the adjusted Specimen template to the caTissue app updateObject.
   * +CaRuby::Writer.update+ will propagate the Specimen update to the changed position dependent,
     which in turn saves via the {CaTissue::TransferEventParameters} proxy.
   * The proxy save will in turn refetch the proxied Specimen position to obtain the identifier
     and merge this into the Specimen position.
   * The Specimen update template is used solely to satisfy the often arcane caTissue interaction
     requirements like this work-around, and is thrown away along with its aberrant state.

   This work-around is the only case of a save template modification to handle a position special
   case. Note that the {CaTissue::SpecimenPosition} logic does not apply to a
   {CaTissue::ContainerPosition}, which can be updated directly.

   The additional complexity of this work-around is necessitated by the caTissue policy of update
   by indirect server-side side-effects that are not reflected back to the client. The caRuby
   declarative API policy persists the save argument as given and reflects the changed database
   state. That policy requires this work-around.

 @quirk caTissue  Bug #63: A SpecimenCollectionGroup update requires that the referenced
   CollectionProtocolRegistration hold extraneous content, including the CPR collection
   protocol and PPI.

 @quirk caTissue Bug: CollectionProtocolRegistration must cascade through the
   CollectionProtocol, but the CP events cannot cascade to SpecimenRequirement without
   raising an Exception. The work-around is to clear the template CP events.

 @quirk caTissue Update SpecimenCollectionGroup requires a collection and received event parameter,
   even if the collection status is pending. Work-around is to add default parameters.

 @quirk caTissue When caTissue updates a Specimen referencing a child Specimen with an identifier which
   is setting the collection status to +Collected+ and has a received or collection event parameters
   without an identifier, then caTissue creates the referenced event parameters as well as spurious
   auto-generated received and collection event parameters. This behavior differs from the top-level
   Specimen, where the event parameters in the argument are simply ignored. The work-around is to
   recursively strip the derived received and collection event parameters, then fetch, match and resave
   the stripped event parameters. This behavior is not entirely confirmed, because the various forms
   of caTissue event parameters corruption are hard to isolate and catalog. The best recourse is to
   assume that caTissue will ignore or corrupt any received and collection event parameters references
   and strip, fetch, match and resave these event parameters separately.

 @quirk caTissue When caTissue updates a pending SCG to status Complete then a collection and
   received event parameters is added to each referenced top-level specimen, even though the
   specimen status is not updated from Pending to Collected. Event parameters are not added to
   child specimens.

 @param obj [Resource] obj the object to save
 @param [Resource] template the template to submit to caCORE
 @raise DatabaseError if the object to save is an {Annotation::Proxy}, which is not supported