--
-- acs-kernel/sql/acs-objects-create.sql
--
-- A base object type that provides auditing columns, permissioning,
-- attributes, and relationships to any subtypes.
--
-- @author Michael Yoon (michael@arsdigita.com)
-- @author Rafael Schloming (rhs@mit.edu)
-- @author Jon Salz (jsalz@mit.edu)
--
-- @creation-date 2000-05-18
--
-- @cvs-id $Id: acs-objects-create.sql,v 1.73 2024/09/11 06:15:47 gustafn Exp $
--

-----------------------------
-- PREDEFINED OBJECT TYPES --
-----------------------------



--
-- procedure inline_0/0
--
CREATE OR REPLACE FUNCTION inline_0(

) RETURNS integer AS $$
DECLARE
 attr_id acs_attributes.attribute_id%TYPE;
BEGIN
 --
 -- The ultimate supertype: object
 --
 PERFORM acs_object_type__create_type (
   'acs_object',
   'Object',
   'Objects',
   null,
   'acs_objects',
   'object_id',
   'acs_object',
   'f',
   null,
   'acs_object.default_name'
   );

 attr_id := acs_attribute__create_attribute (
   'acs_object',
   'object_type',
   'string',
   'Object Type',
   'Object Types',
   null,
   null,
   null,   
   1,
   1,
   null,
   'type_specific',
   'f'
   );

 attr_id := acs_attribute__create_attribute (
   'acs_object',
   'creation_date',
   'date',
   'Created Date',
   null,
   null,
   null,
   null,
   1,
   1,
   null,
   'type_specific',
   'f'
   );

 attr_id := acs_attribute__create_attribute (
   'acs_object',
   'creation_ip',
   'string',
   'Creation IP Address',
   null,
   null,
   null,
   null,
   1,
   1,
   null,
   'type_specific',
   'f'
   );

 attr_id := acs_attribute__create_attribute (
   'acs_object',
   'last_modified',
   'date',
   'Last Modified On',
   null,
   null,
   null,
   null,
   1,
   1,
   null,
   'type_specific',
   'f'
   );

 attr_id := acs_attribute__create_attribute (
   'acs_object',
   'modifying_ip',
   'string',
   'Modifying IP Address',
   null,
   null,
   null,
   null,
   1,
   1,
   null,
   'type_specific',
   'f'
   );

 attr_id := acs_attribute__create_attribute (
   'acs_object',
   'modifying_user',
   'integer',
   'Modifying User',
   null,
   null,
   null,
   null,
   1,
   1,
   null,
   'type_specific',
   'f'
   );

 attr_id := acs_attribute__create_attribute (
	'acs_object',
	'creation_user',
	'integer',
	'Creation user',
	'Creation users',
	null,
	null,
	null,
	0,
	1,
	null,
	'type_specific',
	'f'
	);

 attr_id := acs_attribute__create_attribute (
	'acs_object',
	'context_id',
	'integer',
	'Context ID',
	'Context IDs',
	null,
	null,
	null,
	0,
	1,
	null,
	'type_specific',
	'f'
	);

 attr_id := acs_attribute__create_attribute (
	'acs_object',
	'package_id',
	'integer',
	'Package ID',
	'Package IDs',
	null,
	null,
	null,
	0,
	1,
	null,
	'type_specific',
	'f'
	);

 attr_id := acs_attribute__create_attribute (
	'acs_object',
	'title',
	'string',
	'Title',
	'Titles',
	null,
	null,
	null,
	0,
	1,
	null,
	'type_specific',
	'f'
	);

  return 0;
END;
$$ LANGUAGE plpgsql;

select inline_0 ();

drop function inline_0 ();


-- show errors

-- ******************************************************************
-- * OPERATIONAL LEVEL
-- ******************************************************************

-------------
-- OBJECTS --
-------------

create sequence t_acs_object_id_seq;
create view acs_object_id_seq as
select nextval('t_acs_object_id_seq') as nextval;

create table acs_objects (
	object_id		integer not null
				constraint acs_objects_object_id_pk primary key,
	object_type		varchar(1000) not null
				constraint acs_objects_object_type_fk
				references acs_object_types (object_type),
        title			varchar(1000) default null,
        package_id		integer default null,
        context_id		integer
				CONSTRAINT acs_objects_context_id_fk
				REFERENCES acs_objects(object_id) ON DELETE CASCADE,
	security_inherit_p	boolean default 't' not null,
	creation_user		integer,
	creation_date		timestamptz default current_timestamp not null,
	creation_ip		varchar(50),
	last_modified		timestamptz default current_timestamp not null,
	modifying_user		integer,
	modifying_ip		varchar(50),
        constraint acs_objects_context_object_un
	unique (context_id, object_id)
);

--
-- Avoid potential loops on context_ids. A context_id must be
-- different from the object_id. If no context_id should be checked, its
-- value must be NULL. Note that this constraint is not guaranteed to
-- avoid all loops; it is still possible to create indirect recursive
-- loops but excludes some real-world problems.
ALTER TABLE acs_objects ADD CONSTRAINT acs_objects_context_id_ck CHECK (context_id != object_id);

-- The unique constraint above will force create of this index...
-- create index acs_objects_context_object_idx onacs_objects (context_id, object_id);

create index acs_objects_creation_user_idx on acs_objects (creation_user);
create index acs_objects_creation_date_idx on acs_objects (creation_date);
create index acs_objects_modify_user_idx on acs_objects (modifying_user);
create index acs_objects_last_modified_idx on acs_objects (last_modified);

create index acs_objects_package_idx on acs_objects (package_id);
create index acs_objects_title_idx on acs_objects(title);

create index acs_objects_object_type_idx on acs_objects (object_type);


CREATE OR REPLACE FUNCTION acs_objects_mod_ip_insert_tr () RETURNS trigger AS $$
BEGIN
  new.modifying_ip := new.creation_ip;

  return new;

END;
$$ LANGUAGE plpgsql;

create trigger acs_objects_mod_ip_insert_tr before insert on acs_objects
for each row execute procedure acs_objects_mod_ip_insert_tr ();

-- show errors

CREATE OR REPLACE FUNCTION acs_objects_last_mod_update_tr () RETURNS trigger AS $$
BEGIN
  if new.last_modified is null then
     new.last_modified := old.last_modified;
  elsif new.last_modified = old.last_modified then
     new.last_modified := now();
  end if;
  return new;
END;
$$ LANGUAGE plpgsql;

create trigger acs_objects_last_mod_update_tr before update on acs_objects
for each row execute procedure acs_objects_last_mod_update_tr ();

-- show errors

comment on table acs_objects is '
The root table for the acs object hierarchy.  It all starts here folks.
';

comment on column acs_objects.context_id is $$
 The context_id column points to an object that provides a context for
 this object. Often this will reflect an observed hierarchy in a site,
 for example a bboard message would probably list a bboard topic as
 its context, and a bboard topic might list a sub-site as its
 context. Whenever we ask a question of the form "can user X perform
 action Y on object Z", the acs security model will defer to an
 object's context if there is no information about user X's
 permission to perform action Y on object Z.
$$;

comment on column acs_objects.creation_user is '
 Who created the object; may be null since objects can be created by
 automated processes
';

comment on column acs_objects.modifying_user is '
 Who last modified the object
';

comment on column acs_objects.package_id is '
 Which package instance this object belongs to.
 Please note that in mid-term this column will replace all
 package_ids of package specific tables.
';

comment on column acs_objects.title is '
 Title of the object if applicable.
 Please note that in mid-term this column will replace all
 titles or object_names of package specific tables.
';

----------------------
-- ATTRIBUTE VALUES --
----------------------

create sequence t_acs_attribute_value_id_seq;
create view acs_attribute_value_id_seq as
select nextval('t_acs_attribute_value_id_seq') as nextval;

create table acs_attribute_values (
	object_id	integer not null
			constraint acs_attr_values_obj_id_fk
			references acs_objects (object_id) on delete cascade,
	attribute_id	integer not null
			constraint acs_attr_values_attr_id_fk
			references acs_attributes (attribute_id),
	attr_value	text,
	constraint acs_attribute_values_pk primary key
	(object_id, attribute_id)
);

create index acs_attr_values_attr_id_idx on acs_attribute_values (attribute_id);

comment on table acs_attribute_values is '
  Instead of coercing everything into a big string, we could use
  a "union", i.e, a string column, a number column, a date column,
  and a discriminator.
';

create table acs_static_attr_values (
	object_type	varchar(1000) not null
			constraint acs_static_a_v_obj_id_fk
			references acs_object_types (object_type) on delete cascade,
	attribute_id	integer not null
			constraint acs_static_a_v_attr_id_fk
			references acs_attributes (attribute_id),
	attr_value	text,
	constraint acs_static_a_v_pk primary key
	(object_type, attribute_id)
);

create index acs_stat_attrs_attr_id_idx on acs_static_attr_values (attribute_id);

comment on table acs_static_attr_values is '
  Stores static values for the object attributes. One row per object
  type.
';

------------------------
-- ACS_OBJECT PACKAGE --
------------------------

--
-- Create an SQL schema to allow the same dot notation as in
-- Oracle. The advantage of this notation is that the function can be
-- called identically for PostgreSQL and Oracle, so much duplicated
-- code can be removed.
--
--
-- TODO: handling of schema names in define_function_args, port all
-- acs_object api to the dot notation.
--
CREATE SCHEMA acs_object;


select define_function_args('acs_object__initialize_attributes','object_id');



--
-- procedure acs_object__initialize_attributes/1
--
CREATE OR REPLACE FUNCTION acs_object__initialize_attributes(
   initialize_attributes__object_id integer
) RETURNS integer AS $$
DECLARE
  v_object_type                                 acs_objects.object_type%TYPE;
BEGIN
   if  initialize_attributes__object_id is null then 
	raise exception 'acs_object__initialize_attributes called with null object_id';
   end if;

   -- Initialize dynamic attributes
   insert into acs_attribute_values
    (object_id, attribute_id, attr_value)
   select
    initialize_attributes__object_id, a.attribute_id, a.default_value
   from acs_attributes a, acs_objects o
   where a.object_type = o.object_type
   and o.object_id = initialize_attributes__object_id
   and a.storage = 'generic'
   and a.static_p = 'f';

   -- Retrieve type for static attributes
   select object_type into v_object_type from acs_objects
     where object_id = initialize_attributes__object_id;

   -- Initialize static attributes
   -- begin
     insert into acs_static_attr_values
      (object_type, attribute_id, attr_value)
     select
      v_object_type, a.attribute_id, a.default_value
     from acs_attributes a, acs_objects o
     where a.object_type = o.object_type
       and o.object_id = initialize_attributes__object_id
       and a.storage = 'generic'
       and a.static_p = 't'
       and not exists (select 1 from acs_static_attr_values
                       where object_type = a.object_type);
   -- exception when no_data_found then null;

   return 0; 
END;
$$ LANGUAGE plpgsql;

-- function new


-- old define_function_args('acs_object__new','object_id,object_type;acs_object,creation_date,creation_user,creation_ip,context_id,security_inherit_p;t,title,package_id')
-- new
select define_function_args('acs_object__new','object_id;null,object_type;acs_object,creation_date;now(),creation_user;null,creation_ip;null,context_id;null,security_inherit_p;t,title;null,package_id;null');




--
-- procedure acs_object__new/9
--
CREATE OR REPLACE FUNCTION acs_object__new(
   new__object_id integer,          -- default null
   new__object_type varchar,        -- default 'acs_object'
   new__creation_date timestamptz,  -- default now()
   new__creation_user integer,      -- default null
   new__creation_ip varchar,        -- default null
   new__context_id integer,         -- default null
   new__security_inherit_p boolean, -- default 't'
   new__title varchar,              -- default null
   new__package_id integer          -- default null

) RETURNS integer AS $$
DECLARE
  v_object_id                 acs_objects.object_id%TYPE;
  v_creation_date	      timestamptz;
  v_title                     acs_objects.title%TYPE;
  v_object_type_pretty_name   acs_object_types.pretty_name%TYPE;
BEGIN
  if new__object_id is null then
    select nextval('t_acs_object_id_seq') into v_object_id;
  else
    v_object_id := new__object_id;
  end if;

  if new__title is null then
   select pretty_name
   into v_object_type_pretty_name
   from acs_object_types
   where object_type = new__object_type;

    v_title := v_object_type_pretty_name || ' ' || v_object_id;
  else
    v_title := new__title;
  end if;

  if new__creation_date is null then
   v_creation_date:= now();
  else
   v_creation_date := new__creation_date;
  end if;

  insert into acs_objects
   (object_id, object_type, title, package_id, context_id,
    creation_date, creation_user, creation_ip, security_inherit_p)
  values
   (v_object_id, new__object_type, v_title, new__package_id, new__context_id,
    v_creation_date, new__creation_user, new__creation_ip, 
    new__security_inherit_p);

  PERFORM acs_object__initialize_attributes(v_object_id);

  return v_object_id;
  
END;
$$ LANGUAGE plpgsql;

-- function new



--
-- procedure acs_object__new/6
--
CREATE OR REPLACE FUNCTION acs_object__new(
   new__object_id integer,         -- default null
   new__object_type varchar,       -- default 'acs_object'
   new__creation_date timestamptz, -- default now()
   new__creation_user integer,     -- default null
   new__creation_ip varchar,       -- default null
   new__context_id integer         -- default null

) RETURNS integer AS $$
--
-- acs_object__new/6 maybe obsolete, when we define proper defaults for /9
--
DECLARE
  v_object_id                 acs_objects.object_id%TYPE;
  v_creation_date	      timestamptz;
BEGIN
  return acs_object__new(new__object_id, new__object_type, new__creation_date,
                         new__creation_user, new__creation_ip, new__context_id,
                         't', null, null);
END;
$$ LANGUAGE plpgsql;



--
-- procedure acs_object__new/7
--
CREATE OR REPLACE FUNCTION acs_object__new(
   new__object_id integer,         -- default null
   new__object_type varchar,       -- default 'acs_object'
   new__creation_date timestamptz, -- default now()
   new__creation_user integer,     -- default null
   new__creation_ip varchar,       -- default null
   new__context_id integer,        -- default null
   new__security_inherit_p boolean -- default 't'

) RETURNS integer AS $$
--
-- acs_object__new/7 maybe obsolete, when we define proper defaults for /9
--
DECLARE
BEGIN
  return acs_object__new(new__object_id, new__object_type, new__creation_date,
                         new__creation_user, new__creation_ip, new__context_id,
                         new__security_inherit_p, null, null);
END;
$$ LANGUAGE plpgsql;



--
-- procedure acs_object__new/8
--
CREATE OR REPLACE FUNCTION acs_object__new(
   new__object_id integer,          -- default null
   new__object_type varchar,        -- default 'acs_object'
   new__creation_date timestamptz,  -- default now()
   new__creation_user integer,      -- default null
   new__creation_ip varchar,        -- default null
   new__context_id integer,         -- default null
   new__security_inherit_p boolean, -- default 't'
   new__title varchar               -- default null

) RETURNS integer AS $$
--
-- acs_object__new/8 maybe obsolete, when we define proper defaults for /9
--
DECLARE
BEGIN
  return acs_object__new(new__object_id, new__object_type, new__creation_date,
                         new__creation_user, new__creation_ip, new__context_id,
                         new__security_inherit_p, new__title, null);
END;
$$ LANGUAGE plpgsql;



--
-- procedure acs_object__new/8
--
CREATE OR REPLACE FUNCTION acs_object__new(
   new__object_id integer,         -- default null
   new__object_type varchar,       -- default 'acs_object'
   new__creation_date timestamptz, -- default now()
   new__creation_user integer,     -- default null
   new__creation_ip varchar,       -- default null
   new__context_id integer,        -- default null
   new__title varchar,             -- default null
   new__package_id integer         -- default null

) RETURNS integer AS $$
--
-- acs_object__new/8 maybe obsolete, when we define proper defaults for /9
--
DECLARE
BEGIN
  return acs_object__new(new__object_id, new__object_type, new__creation_date,
                         new__creation_user, new__creation_ip, new__context_id,
                         't', new__title, new__package_id);
END;
$$ LANGUAGE plpgsql;



--
-- procedure acs_object__new/7
--
CREATE OR REPLACE FUNCTION acs_object__new(
   new__object_id integer,         -- default null
   new__object_type varchar,       -- default 'acs_object'
   new__creation_date timestamptz, -- default now()
   new__creation_user integer,     -- default null
   new__creation_ip varchar,       -- default null
   new__context_id integer,        -- default null
   new__title varchar              -- default null

) RETURNS integer AS $$
--
-- acs_object__new/7 maybe obsolete, when we define proper defaults for /9
--
DECLARE
BEGIN
  return acs_object__new(new__object_id, new__object_type, new__creation_date,
                         new__creation_user, new__creation_ip, new__context_id,
                         't', new__title, null);
END;
$$ LANGUAGE plpgsql;



--
-- procedure acs_object__new/2
--
CREATE OR REPLACE FUNCTION acs_object__new(
   object_id integer,  -- default null
   object_type varchar -- default 'acs_object'

) RETURNS integer AS $$
--
-- acs_object__new/2 maybe obsolete, when we define proper defaults for /9
--
DECLARE
BEGIN
        return acs_object__new(object_id,object_type,now(),null,null,null);
END;
$$ LANGUAGE plpgsql;


-- procedure delete

select define_function_args('acs_object__delete','object_id');

--
-- procedure acs_object__delete/1
--
CREATE OR REPLACE FUNCTION acs_object__delete(
   delete__object_id integer
) RETURNS integer AS $$
DECLARE
  obj_type record;
BEGIN

   -- Also child relationships must be deleted. On delete cascade
   -- would not help here, as only tuple in acs_rels would go, while
   -- related acs_object would stay.
   PERFORM acs_object__delete(object_id)
     from acs_objects where object_id in
     (select rel_id from acs_rels where
          object_id_one = delete__object_id or
          object_id_two = delete__object_id);

  -- GN: the following deletion operation iterates over the id_columns
  -- of the acs_object_types of the type tree for the object and
  -- performs manual deletions in these tables by trying to delete the
  -- delete__object_id from the id_column.  This deletion includes as
  -- well the deletion in acs_objects.
  --
  -- In the best of all possible worlds, this would not
  -- be necessary, when the objects would have specified "on delete
  -- cascade" for the id_columns.

  for obj_type
  in select ot2.table_name, ot2.id_column
       from acs_object_types ot1, acs_object_types ot2
       where ot1.object_type = (select object_type
                               from acs_objects o
                               where o.object_id = delete__object_id)
         and ot1.tree_sortkey between ot2.tree_sortkey and tree_right(ot2.tree_sortkey)
    order by ot2.tree_sortkey desc
  loop
    -- Delete from the table.

    -- DRB: I removed the quote_ident calls that DanW originally included
    -- because the table names appear to be stored in upper case.  Quoting
    -- causes them to not match the actual lower or potentially mixed-case
    -- table names.  We will just forbid squirrely names that include quotes.
    
    -- daveB
    -- ETP is creating a new object, but not a table, although it does specify a
    -- table name, so we need to check if the table exists. Wp-slim does this too

    if table_exists(obj_type.table_name) then
      execute 'delete from ' || obj_type.table_name ||
          ' where ' || obj_type.id_column || ' =  ' || delete__object_id;
    end if;
  end loop;

  return 0; 
END;
$$ LANGUAGE plpgsql;


--
-- procedure acs_object__name/1
--
CREATE OR REPLACE FUNCTION acs_object.name(
   name__object_id integer
) RETURNS varchar AS $$
DECLARE
  object_name            varchar;  
  v_object_id            integer;
  obj_type               record;  
  obj                    record;      
BEGIN
  -- Find the name function for this object, which is stored in the
  -- name_method column of acs_object_types. Starting with this
  -- object's actual type, traverse the type hierarchy upwards until
  -- a non-null name_method value is found.
  --
  -- select name_method
  --  from acs_object_types
  -- start with object_type = (select object_type
  --                             from acs_objects o
  --                            where o.object_id = name__object_id)
  -- connect by object_type = prior supertype

  select title into object_name
  from acs_objects
  where object_id = name__object_id;

  if (object_name is not null) then
    return object_name;
  end if;

  for obj_type
  in select ot2.name_method
        from acs_object_types ot1, acs_object_types ot2
       where ot1.object_type = (select object_type
                                 from acs_objects o
                                where o.object_id = name__object_id)
         and ot1.tree_sortkey between ot2.tree_sortkey and tree_right(ot2.tree_sortkey)
    order by ot2.tree_sortkey desc
  loop
   if obj_type.name_method != '' and obj_type.name_method is NOT null then

    -- Execute the first name_method we find (since we're traversing
    -- up the type hierarchy from the object's exact type) using
    -- Native Dynamic SQL, to ascertain the name of this object.
    --
    --execute 'select ' || object_type.name_method || '(:1) from dual'

    for obj in execute 'select ' || obj_type.name_method || '(' || name__object_id || ')::varchar as object_name' loop
        object_name := obj.object_name;
        exit;
    end loop;

    exit;
   end if;
  end loop;

  return object_name;
  
END;
$$ LANGUAGE plpgsql stable strict;

-- Backward compatibility definition for acs_object.name
select define_function_args('acs_object__name','object_id');

CREATE OR REPLACE FUNCTION acs_object__name(
   name__object_id integer
) RETURNS varchar AS $$
BEGIN
  RETURN acs_object.name(name__object_id);
END;
$$ LANGUAGE plpgsql stable strict;



-- function default_name

select define_function_args('acs_object__default_name','object_id');



--
-- procedure acs_object__default_name/1
--
CREATE OR REPLACE FUNCTION acs_object__default_name(
   default_name__object_id integer
) RETURNS varchar AS $$
DECLARE
  object_type_pretty_name   acs_object_types.pretty_name%TYPE;
BEGIN
  select ot.pretty_name
  into object_type_pretty_name
  from acs_objects o, acs_object_types ot
  where o.object_id = default_name__object_id
  and o.object_type = ot.object_type;

  return object_type_pretty_name || ' ' || default_name__object_id;

END;
$$ LANGUAGE plpgsql stable strict;


-- function package_id

select define_function_args('acs_object__package_id','object_id');

--
-- procedure acs_object__package_id/1
--
CREATE OR REPLACE FUNCTION acs_object__package_id(
   p_object_id integer
) RETURNS integer AS $$
DECLARE
  v_package_id acs_objects.package_id%TYPE;
BEGIN
  if p_object_id is null then
    return null;
  end if;

  select package_id into v_package_id
  from acs_objects
  where object_id = p_object_id;

  return v_package_id;
END;
$$ LANGUAGE plpgsql stable strict;


-- procedure get_attribute_storage

select define_function_args('acs_object__get_attribute_storage','object_id_in,attribute_name_in');



--
-- procedure acs_object__get_attribute_storage/2
--
CREATE OR REPLACE FUNCTION acs_object__get_attribute_storage(
   object_id_in integer,
   attribute_name_in varchar
) RETURNS text AS $$
DECLARE

--  these three are the out variables
  v_column               varchar;  
  v_table_name           varchar;  
  v_key_sql              text;
  
  v_object_type          acs_attributes.object_type%TYPE;
  v_static               acs_attributes.static_p%TYPE;
  v_attr_id              acs_attributes.attribute_id%TYPE;
  v_storage              acs_attributes.storage%TYPE;
  v_attr_name            acs_attributes.attribute_name%TYPE;
  v_id_column            varchar(200);   
  v_sql                  text;  
  v_return               text;  
  v_rec                  record;
BEGIN
   --   select 
   --     object_type, id_column
   --   from
   --     acs_object_types
   --   connect by
   --     object_type = prior supertype
   --   start with
   --     object_type = (select object_type from acs_objects 
   --                    where object_id = object_id_in)

   -- Determine the attribute parameters
   select
     a.attribute_id, a.static_p, a.storage, a.table_name, a.attribute_name,
     a.object_type, a.column_name, t.id_column 
   into 
     v_attr_id, v_static, v_storage, v_table_name, v_attr_name, 
     v_object_type, v_column, v_id_column
   from 
     acs_attributes a,
     (select ot2.object_type, ot2.id_column
       from acs_object_types ot1, acs_object_types ot2
      where ot1.object_type = (select object_type
                                from acs_objects o
                               where o.object_id = object_id_in)
        and ot1.tree_sortkey between ot2.tree_sortkey and tree_right(ot2.tree_sortkey)
     ) t
   where   
     a.attribute_name = attribute_name_in
   and
     a.object_type = t.object_type;

   if NOT FOUND then 
      raise EXCEPTION '-20000: No such attribute % for object % in acs_object.get_attribute_storage.', attribute_name_in, object_id_in;
   end if;

   -- This should really be done in a trigger on acs_attributes,
   -- instead of generating it each time in this function

   -- If there is no specific table name for this attribute,
   -- figure it out based on the object type
   if v_table_name is null or v_table_name = '' then

     -- Determine the appropriate table name
     if v_storage = 'generic' then
       -- Generic attribute: table name/column are hardcoded

       v_column := 'attr_value';

       if v_static = 'f' then
         v_table_name := 'acs_attribute_values';
         v_key_sql := '(object_id = ' || object_id_in || ' and ' ||
                      'attribute_id = ' || v_attr_id || ')';
       else
         v_table_name := 'acs_static_attr_values';
         v_key_sql := '(object_type = ''' || v_object_type || ''' and ' ||
                      'attribute_id = ' || v_attr_id || ')';
       end if;

     else
       -- Specific attribute: table name/column need to be retrieved
 
       if v_static = 'f' then
         select 
           table_name, id_column 
         into 
           v_table_name, v_id_column
         from 
           acs_object_types 
         where 
           object_type = v_object_type;
         if NOT FOUND then 
            raise EXCEPTION '-20000: No data found for attribute %::% object_id % in acs_object.get_attribute_storage', v_object_type, attribute_name_in, object_id_in;
         end if;
       else
         raise EXCEPTION '-20000: No table name specified for storage specific static attribute %::% object_id % in acs_object.get_attribute_storage.',v_object_type, attribute_name_in, object_id_in;
       end if;
  
     end if;
   else 
     -- There is a custom table name for this attribute.
     -- Get the id column out of the acs_object_tables
     -- Raise an error if not found
     select id_column into v_id_column from acs_object_type_tables
       where object_type = v_object_type 
       and table_name = v_table_name;
       if NOT FOUND then 
          raise EXCEPTION '-20000: No data found for attribute %::% object_id % in acs_object.get_attribute_storage', v_object_type, attribute_name_in, object_id_in;
       end if;
   end if;

   if v_column is null or v_column = '' then

     if v_storage = 'generic' then
       v_column := 'attr_value';
     else
       v_column := v_attr_name;
     end if;

   end if;

   if v_key_sql is null or v_key_sql = '' then
     if v_static = 'f' then   
       v_key_sql := v_id_column || ' = ' || object_id_in ; 
     else
       v_key_sql := v_id_column || ' = ''' || v_object_type || '''';
     end if;
   end if;

   return v_column || ',' || v_table_name || ',' || v_key_sql; 

END;
$$ LANGUAGE plpgsql stable;


select define_function_args('acs_object__get_attr_storage_column','v_vals');



--
-- procedure acs_object__get_attr_storage_column/1
--
CREATE OR REPLACE FUNCTION acs_object__get_attr_storage_column(
   v_vals text
) RETURNS text AS $$
DECLARE
        v_idx   integer;
BEGIN
        v_idx := strpos(v_vals,',');
        if v_idx = 0 or v_vals is null then 
           raise exception 'invalid storage format: acs_object.get_attr_storage_column %',v_vals;
        end if;

        return substr(v_vals,1,v_idx - 1);

END;
$$ LANGUAGE plpgsql immutable;

select define_function_args('acs_object__get_attr_storage_table','v_vals');



--
-- procedure acs_object__get_attr_storage_table/1
--
CREATE OR REPLACE FUNCTION acs_object__get_attr_storage_table(
   v_vals text
) RETURNS text AS $$
DECLARE
        v_idx   integer;
        v_tmp   varchar;
BEGIN
        v_idx := strpos(v_vals,',');

        if v_idx = 0 or v_vals is null then 
           raise exception 'invalid storage format: acs_object.get_attr_storage_table %',v_vals;
        end if;

        v_tmp := substr(v_vals,v_idx + 1);
        v_idx := strpos(v_tmp,',');
        if v_idx = 0 then 
           raise exception 'invalid storage format: acs_object.get_attr_storage_table %',v_vals;
        end if;

        return substr(v_tmp,1,v_idx - 1);

END;
$$ LANGUAGE plpgsql immutable;

select define_function_args('acs_object__get_attr_storage_sql','v_vals');



--
-- procedure acs_object__get_attr_storage_sql/1
--
CREATE OR REPLACE FUNCTION acs_object__get_attr_storage_sql(
   v_vals text
) RETURNS text AS $$
DECLARE
        v_idx   integer;
        v_tmp   varchar;
BEGIN
        v_idx := strpos(v_vals, ',');

        if v_idx = 0 or v_vals is null then 
           raise exception 'invalid storage format: acs_object.get_attr_storage_sql %',v_vals;
        end if;

        v_tmp := substr(v_vals, v_idx + 1);
        v_idx := strpos(v_tmp, ',');
        if v_idx = 0 then 
           raise exception 'invalid storage format: acs_object.get_attr_storage_sql %',v_vals;
        end if;

        return substr(v_tmp, v_idx + 1);

END;
$$ LANGUAGE plpgsql immutable;

-- function get_attribute

select define_function_args('acs_object__get_attribute','object_id_in,attribute_name_in');



--
-- procedure acs_object__get_attribute/2
--
CREATE OR REPLACE FUNCTION acs_object__get_attribute(
   object_id_in integer,
   attribute_name_in varchar
) RETURNS text AS $$
DECLARE
  v_table_name           varchar(200);  
  v_column               varchar(200);  
  v_key_sql              text; 
  v_return               text; 
  v_storage              text;
  v_rec                  record;
BEGIN

   v_storage := acs_object__get_attribute_storage(object_id_in, attribute_name_in);

   v_column     := acs_object__get_attr_storage_column(v_storage);
   v_table_name := acs_object__get_attr_storage_table(v_storage);
   v_key_sql    := acs_object__get_attr_storage_sql(v_storage);

   for v_rec in execute 'select ' || quote_ident(v_column) || '::text as column_return from ' || quote_ident(v_table_name) || ' where ' || v_key_sql
      LOOP
        v_return := v_rec.column_return;
        exit;
   end loop;
   if not FOUND then 
       return null;
   end if;

   return v_return;

END;
$$ LANGUAGE plpgsql stable;


-- procedure set_attribute

select define_function_args('acs_object__set_attribute','object_id_in,attribute_name_in,value_in');



--
-- procedure acs_object__set_attribute/3
--
CREATE OR REPLACE FUNCTION acs_object__set_attribute(
   object_id_in integer,
   attribute_name_in varchar,
   value_in varchar
) RETURNS integer AS $$
DECLARE
  v_table_name           varchar;  
  v_column               varchar;  
  v_key_sql              text; 
  v_return               text; 
  v_storage              text;
BEGIN

   v_storage    := acs_object__get_attribute_storage(object_id_in, attribute_name_in);
   v_column     := acs_object__get_attr_storage_column(v_storage);
   v_table_name := acs_object__get_attr_storage_table(v_storage);
   v_key_sql    := acs_object__get_attr_storage_sql(v_storage);

   if value_in is null then
      execute 'update ' || v_table_name || ' set ' || v_column || ' = NULL where ' || v_key_sql;   
   else
      execute 'update ' || v_table_name || ' set ' || quote_ident(v_column) || ' = ' || quote_literal(value_in) || ' where ' || v_key_sql;
   end if;
   
   return 0; 
END;
$$ LANGUAGE plpgsql;


-- function check_object_ancestors


-- added
select define_function_args('acs_object__check_object_ancestors','object_id,ancestor_id,n_generations');

--
-- procedure acs_object__check_object_ancestors/3
--
CREATE OR REPLACE FUNCTION acs_object__check_object_ancestors(
   check_object_ancestors__object_id integer,
   check_object_ancestors__ancestor_id integer,
   check_object_ancestors__n_generations integer
) RETURNS boolean AS $$
DECLARE
  check_object_ancestors__context_id             acs_objects.context_id%TYPE;
  check_object_ancestors__security_inherit_p     acs_objects.security_inherit_p%TYPE;
  n_rows                                         integer;       
  n_gens                                         integer;       
  result                                         boolean;       
BEGIN
   -- OBJECT_ID is the object we are verifying
   -- ANCESTOR_ID is the current ancestor we are tracking
   -- N_GENERATIONS is how far ancestor_id is from object_id

   -- Note that this function is only supposed to verify that the
   -- index contains each ancestor for OBJECT_ID. It doesn't
   -- guarantee that there aren''t extraneous rows or that
   -- OBJECT_ID's children are contained in the index. That is
   -- verified by separate functions.

   result := 't';

   -- Grab the context and security_inherit_p flag of the current
   -- ancestor's parent.
   select context_id, security_inherit_p 
   into check_object_ancestors__context_id, 
        check_object_ancestors__security_inherit_p
   from acs_objects
   where object_id = check_object_ancestors__ancestor_id;

   if check_object_ancestors__ancestor_id = 0 then
     if check_object_ancestors__context_id is null then
       result := 't';
     else
       -- This can be a constraint, can't it?
       PERFORM acs_log__error('acs_object.check_representation',
                     'Object 0 doesn''t have a null context_id');
       result := 'f';
     end if;
   else
     if check_object_ancestors__context_id is null or 
        check_object_ancestors__security_inherit_p = 'f' 
     THEN
       check_object_ancestors__context_id := 0;
     end if;

     if acs_object__check_context_index(check_object_ancestors__object_id, 
                                        check_object_ancestors__ancestor_id, 
                                        check_object_ancestors__n_generations) = 'f' then
       result := 'f';
     end if;

     if acs_object__check_object_ancestors(check_object_ancestors__object_id, 
                                           check_object_ancestors__context_id,
	                      check_object_ancestors__n_generations + 1) = 'f' then
       result := 'f';
     end if;
   end if;

   return result;
  
END;
$$ LANGUAGE plpgsql;


-- function check_object_descendants


-- added
select define_function_args('acs_object__check_object_descendants','object_id,descendant_id,n_generations');

--
-- procedure acs_object__check_object_descendants/3
--
CREATE OR REPLACE FUNCTION acs_object__check_object_descendants(
   object_id integer,
   descendant_id integer,
   n_generations integer
) RETURNS boolean AS $$
DECLARE
  result                 boolean;     
  obj                    record;  
BEGIN
   -- OBJECT_ID is the object we are verifying.
   -- DESCENDANT_ID is the current descendant we are tracking.
   -- N_GENERATIONS is how far the current DESCENDANT_ID is from
   -- OBJECT_ID.

   -- This function will verify that each actually descendant of
   -- OBJECT_ID has a row in the index table. It does not check that
   -- there aren't extraneous rows or that the ancestors of OBJECT_ID
   -- are maintained correctly.

   result := 't';

   -- First verify that OBJECT_ID and DESCENDANT_ID are actually in
   -- the index.
   if acs_object__check_context_index(descendant_id, object_id, n_generations) = 'f' then
     result := 'f';
   end if;

   -- For every child that reports inheriting from OBJECT_ID we need to call
   -- ourselves recursively.
   for obj in  select *
	       from acs_objects
	       where context_id = descendant_id
	       and security_inherit_p = 't' loop
     if acs_object__check_object_descendants(object_id, obj.object_id,
       n_generations + 1) = 'f' then
       result := 'f';
     end if;
   end loop;

   return result;
  
END;
$$ LANGUAGE plpgsql;


-- function check_path
select define_function_args('acs_object__check_path','object_id,ancestor_id');



--
-- procedure acs_object__check_path/2
--
CREATE OR REPLACE FUNCTION acs_object__check_path(
   check_path__object_id integer,
   check_path__ancestor_id integer
) RETURNS boolean AS $$
DECLARE
  check_path__context_id             acs_objects.context_id%TYPE;
  check_path__security_inherit_p     acs_objects.security_inherit_p%TYPE;
BEGIN
   if check_path__object_id is null or check_path__ancestor_id then
	raise exception 'acs_object__check_path called with null object_id or ancestor_id';
   end if;

   if check_path__object_id = check_path__ancestor_id then
     return 't';
   end if;

   select context_id, security_inherit_p 
   into check_path__context_id, check_path__security_inherit_p
   from acs_objects
   where object_id = check_path__object_id;

   -- we should be able to handle the case where check_path fails 
   -- should we not?

   if check_path__object_id = 0 and check_path__context_id is null then 
      return 'f';
   end if;

   if check_path__context_id is null or check_path__security_inherit_p = 'f' 
   then
     check_path__context_id := 0;
   end if;

   return acs_object__check_path(check_path__context_id, 
                                 check_path__ancestor_id);
  
END;
$$ LANGUAGE plpgsql stable;


--
-- procedure acs_object__update_last_modified/3
--
CREATE OR REPLACE FUNCTION acs_object__update_last_modified(
   acs_object__update_last_modified__object_id integer,
   acs_object__update_last_modified__modifying_user integer,
   acs_object__update_last_modified__modifying_ip varchar
) RETURNS integer AS $$
DECLARE
BEGIN
    return acs_object__update_last_modified(acs_object__update_last_modified__object_id, acs_object__update_last_modified__modifying_user, acs_object__update_last_modified__modifying_ip, now());
END;
$$ LANGUAGE plpgsql;



select define_function_args('acs_object__update_last_modified','object_id,modifying_user,modifying_ip,last_modified;now()');

--
-- procedure acs_object__update_last_modified/4
--
CREATE OR REPLACE FUNCTION acs_object__update_last_modified(
   acs_object__update_last_modified__object_id integer,
   acs_object__update_last_modified__modifying_user integer,
   acs_object__update_last_modified__modifying_ip varchar,
   acs_object__update_last_modified__last_modified timestamptz -- default now()

) RETURNS integer AS $$
DECLARE
    v_parent_id                                          integer;
    v_last_modified                                      timestamptz;
BEGIN
    if acs_object__update_last_modified__last_modified is null then
        v_last_modified := now();
    else
        v_last_modified := acs_object__update_last_modified__last_modified;
    end if;

    update acs_objects
    set last_modified = v_last_modified,
        modifying_user = acs_object__update_last_modified__modifying_user,
        modifying_ip = acs_object__update_last_modified__modifying_ip
    where object_id = acs_object__update_last_modified__object_id;

    select context_id
    into v_parent_id
    from acs_objects
    where object_id = acs_object__update_last_modified__object_id;

    if v_parent_id is not null and v_parent_id != 0 then
        perform acs_object__update_last_modified(v_parent_id, acs_object__update_last_modified__modifying_user, acs_object__update_last_modified__modifying_ip, v_last_modified);
    end if;

    return acs_object__update_last_modified__object_id;
END;
$$ LANGUAGE plpgsql;

-- show errors

-------------------
-- MISCELLANEOUS --
-------------------

create table general_objects (
	object_id		integer not null
				constraint general_objects_object_id_fk
				references acs_objects (object_id)
				constraint general_objects_object_id_pk
				primary key,
	on_which_table		varchar(30) not null,
	on_what_id		integer not null,
	constraint general_objects_un
		unique (on_which_table, on_what_id)
);

comment on table general_objects is '
 This table can be used to treat non-acs_objects as acs_objects for
 purposes of access control, categorization, etc.
';