Porting to ACS 4.0 - A Case Study

by Nick Strugnell

This document describes the steps required to port a single ACS 3.x module, simple survey to ACS 4.0. The total time to do the port, or at least bring the module into a useable and reasonably bug-free state, was 54.5 programmer hours. Note that the programmer in question had no prior knowledge of ACS 4.0, nor of the Oracle object model used extensively in ACS 4.0, but did have some prior knowledge of the internal workings of the simple survey module. Your mileage may vary.

Why?

Why would you want to port a module to ACS 4.0? The new system offers significant advantages over 3.x, notably packaging, a unified permissions model and an object model that simplifies some aspects of implementation.

How?

In the case of the simple survey module I performed the tasks required for the port in the following order:

  1. Reorganised files in accordance with the packaging conventions
  2. Registered the new package on a running ACS 4.0 server
  3. Redesigned the data model to use the object model
  4. Edited pages to use the new permissions and party models

Packaging

The ACS 4.0 packaging systems replaces the sometimes haphazard file layout used in earlier versions. Complete instructions can be found at http://acs40.arsdigita.com/doc/developer-guide/packages.html but in essence your file layout should be as follows:

The packages directory itself is in the server root directory, usually /web/server-name.

Registering the Package

ACS 4.0 provides easy to use package management facilites to associate files, watch TCL libraries for changes and mount packages at arbitrary URLs on the client site. This is managed through the admin interface provided at acs-admin/apm under a standard ACS 4.0 install. The process is self-explanatory: simply click on 'Create a new package' and fill in the resulting form. You will then be presented with a list of files to become part of the package, if extraneous files (e.g. editor backup files) have been selected, simply deselect them. Providing naming conventions have been adhered to, TCL library files should be so identified and you will have the option of setting up a watch on them so that they are automatically reloaded when changed. This is useful during the development process as it negates the need to restart AOLserver every time a change is made in the TCL library.

Mounting the Package

Once a package has been installed it needs to be mounted before it is visible to users. This is done using the site administration at acs-admin. First, select the subsite on which you want to mount an instance of the package and then select Site Map. In a default ACS 4.0 installation there is only one subsite, Main Site and its site map is available at admin/site-map.

We then need to create a new URL under an existing URL - for the infoshare site I created a URL, investor_profiling_tool, under the root. Do this by clicking on the 'new sub folder' option alongisde and existing URL and entering the name for the URL - note that this does not have to be the same as the package key.

Next, we must mount the package. If the package has been mounted before, you may simply select the 'mount' option alongside the new URL and select the package. However, if this is the first time you have mounted the package, select 'new application'. You can then name the application (give it a pretty name rather than the package key) and select the package from a select box. This way, you can have several instances of a package, perhaps some unmounted and some mounted, simultaneously at different URLs on the same site.

Data Model

Rewriting the data model is possibly the hardest part of performing the port, as the object system employed may seem counter-intuitive to programmers who have cut their OOP teeth on C++ or Java. The principal decision to be made is which objects in your module should be converted to the object model, and which should remain as simple tables. There is no clear answer to this, but a rule of thumb is that any table that contains a reference to the users table in ACS 3.x, i.e. any table that you will perform permissions tests on, should be converted to the object model. Additionally, any table which is to be included in the site-wide search should be made on object. In the case of simple survey, I converted the survsimp_surveys (individual surveys), survsimp_questions (questions which make up a survey) and survsimp_responses (collections of answers forming a single users response to a survey) tables to the object model.

All object types are derived from the acs_object type, so we declare a survsimp_survey type using the following PL/SQL block:

begin
    acs_object_type.create_type (
	    	supertype => 'acs_object',
		object_type => 'survsimp_survey',
		pretty_name => 'Simple Survey',
		pretty_plural => 'Simple Surveys',
		table_name => 'SURVSIMP_SURVEYS',
		id_column => 'SURVEY_ID'
    );
end;

The new object type, survsimp_survey, is associated with a table, survsimp_surveys (note the pluralisation), each row of which will be an instance of the survsimp_survey type:

create table survsimp_surveys (
	survey_id		constraint survsimp_surveys_survey_id_fk
				references acs_objects (object_id)
                                constraint survsimp_surveys_pk
				primary key,
	name			varchar(100)
				constraint survsimp_surveys_name_nn
				not null,
	-- short, non-editable name we can identify this survey by
	short_name		varchar(20)
				constraint survsimp_surveys_short_name_u
				unique
				constraint survsimp_surveys_short_name_nn
				not null,
	description		varchar(4000)
				constraint survsimp_surveys_desc_nn
				not null,
        description_html_p      char(1)
                                constraint survsimp_surv_desc_html_p_ck
				check(description_html_p in ('t','f')),
	enabled_p               char(1)
				constraint survsimp_surveys_enabled_p_ck
				check(enabled_p in ('t','f')),
	-- limit to one response per user
	single_response_p	char(1)
				constraint survsimp_sur_single_resp_p_ck
				check(single_response_p in ('t','f')),
	single_editable_p	char(1)
				constraint survsimp_surv_single_edit_p_ck
				check(single_editable_p in ('t','f')),
	type                    varchar(20)
);

Compare this with the survsimp_surveys table in 3.x (items missing in the 4.0 version highlighted):

create sequence survsimp_survey_id_sequence start with 1;

create table survsimp_surveys (
        survey_id               integer primary key,
        name                    varchar(100) not null,
        -- short, non-editable name we can identify this survey by
        short_name              varchar(20) unique not null,
        description             varchar(4000) not null,
        description_html_p      char(1) default 'f'
                                constraint survsimp_sur_desc_html_p_ck
                                check(description_html_p in ('t','f')),
        creation_user           not null references users(user_id),
        creation_date           date default sysdate,
        enabled_p               char(1) default 'f' check(enabled_p in ('t','f')),
        -- limit to one response per user       
        single_response_p       char(1) default 'f' check(single_response_p in ('t','f')),
        single_editable_p       char(1) default 't' check(single_editable_p in ('t','f')),
        type                    varchar2(20) default 'general'
); 

Notice that the ACS 4.0 version has fewer columns. This is because creation_user and creation_date are inherited (kinda) from the acs_object type, and do not need to be included in the survsimp_surveys table. Additionally, the survey_id is now a reference to the object_id column of the acs_objects table and no longer has a sequence associated with it. In fact an instantiation of the survsimp_survey type should always use a survey_id generated by the acs_object_id_seq sequence, as should any type derived from the acs_object type.

Finally a constructor and destructor for the survsimp_survey type are required. These are PL/SQL procedures stored inside a package:

create or replace package survsimp_survey
as
	function new (
		survey_id	in survsimp_surveys.survey_id%TYPE			default null,
		name		in survsimp_surveys.name%TYPE,
		short_name	in survsimp_surveys.short_name%TYPE,
		description	in survsimp_surveys.description%TYPE,
		description_html_p	in survsimp_surveys.description_html_p%TYPE	default 'f',
		single_response_p	in survsimp_surveys.single_response_p%TYPE	default 'f',
		single_editable_p	in survsimp_surveys.single_editable_p%TYPE	default 't',
		enabled_p	in survsimp_surveys.enabled_p%TYPE			default 'f',
		type		in survsimp_surveys.type%TYPE				default 'general',
		object_type	in acs_objects.object_type%TYPE				default 'survsimp_survey',
		creation_date	in acs_objects.creation_date%TYPE			default sysdate,
		creation_user	in acs_objects.creation_user%TYPE			default null,
		creation_ip	in acs_objects.creation_ip%TYPE				default null,
		context_id	in acs_objects.context_id%TYPE				default null
	) return acs_objects.object_id%TYPE;

	procedure delete (
		survey_id in survsimp_surveys.survey_id%TYPE
	);
end survsimp_survey;
/
show errors

create or replace package body survsimp_survey
as
	function new (
		survey_id	in survsimp_surveys.survey_id%TYPE			default null,
		name		in survsimp_surveys.name%TYPE,
		short_name	in survsimp_surveys.short_name%TYPE,
		description	in survsimp_surveys.description%TYPE,
		description_html_p	in survsimp_surveys.description_html_p%TYPE	default 'f',
		single_response_p	in survsimp_surveys.single_response_p%TYPE	default 'f',
		single_editable_p	in survsimp_surveys.single_editable_p%TYPE	default 't',
		enabled_p	in survsimp_surveys.enabled_p%TYPE			default 'f',
		type		in survsimp_surveys.type%TYPE				default 'general',
		object_type	in acs_objects.object_type%TYPE				default 'survsimp_survey',
		creation_date	in acs_objects.creation_date%TYPE			default sysdate,
		creation_user	in acs_objects.creation_user%TYPE			default null,
		creation_ip	in acs_objects.creation_ip%TYPE				default null,
		context_id	in acs_objects.context_id%TYPE				default null
	) return acs_objects.object_id%TYPE
	is
		v_survey_id survsimp_surveys.survey_id%TYPE;
	begin
		v_survey_id := acs_object.new (
			object_id => survey_id,
			object_type => object_type,
			creation_date => creation_date,
			creation_user => creation_user,
			creation_ip => creation_ip,
			context_id => context_id
		);   
		insert into survsimp_surveys
			(survey_id, name, short_name, description, description_html_p,
			single_response_p, single_editable_p, enabled_p, type)
			values
			(v_survey_id, new.name, new.short_name, new.description, new.description_html_p,
			new.single_response_p, new.single_editable_p, new.enabled_p, new.type);

		return v_survey_id;
	end new;

	procedure delete (
		survey_id survsimp_surveys.survey_id%TYPE
	)
	is
	begin
		delete from survsimp_surveys
			where survey_id = survsimp_survey.delete.survey_id;
		acs_object.delete(survey_id);
	end delete;
end survsimp_survey;
/
show errors

The constructor, survsimp_survey.new, and destructor, survsimp_survey.delete, are declared in the create package statement and defined in the the create package body statement. The function of the constructor is to create an instance of acs_object and insert a row referencing that instance to the survsimp_surveys table. The destructor deletes both the acs_object and the survsimp_surveys.

Permissions

Permissions checking under ACS 4.0 has been much simplified and is one of the most compelling reasons to migrate from 3.x. Under ACS 4.0 all permissions checks boil down to the question Can x perform action y on object z?. The actions are defined by the module writer.

The following PL/SQL statements define self explanatory actions on the simple survey package:

begin

	acs_privilege.create_privilege('survsimp_create_survey');
	acs_privilege.create_privilege('survsimp_delete_survey');
        acs_privilege.create_privilege('survsimp_modify_survey');
	acs_privilege.create_privilege('survsimp_create_question');
	acs_privilege.create_privilege('survsimp_delete_question');
        acs_privilege.create_privilege('survsimp_modify_question');
	acs_privilege.create_privilege('survsimp_take_survey');
end;

When the package is registered with the ACS 4.0 package manager, these permissions become available to the permissions manager at /permissions, which can be used assign these permissions to various users. I also created a super-privilege, survsimp_admin_survey:

begin
	acs_privilege.create_privilege('survsimp_admin_survey');

	acs_privilege.add_child('survsimp_admin_survey','survsimp_create_survey');
	acs_privilege.add_child('survsimp_admin_survey','survsimp_delete_survey');
	acs_privilege.add_child('survsimp_admin_survey','survsimp_modify_survey');
	acs_privilege.add_child('survsimp_admin_survey','survsimp_create_question');
	acs_privilege.add_child('survsimp_admin_survey','survsimp_delete_question');
	acs_privilege.add_child('survsimp_admin_survey','survsimp_modify_question');
end;

The acs_privilege.add_child function adds the privilege in the second argument to that in the first, so in this case anyone with the survsimp_admin_survey privilege has full privileges to create, modify and delete surveys and questions.

How are the new permissions used? The simplest way is to use the ad_require_permission TCL procedure. This takes two arguments, an object_id and the privilege required. If the current user does not have the required privilege, he or she is presented with an error message. For example, the page survey-simple/www/one.tcl, which presents a survey to a user who wishes to take it, contains the single line:

ad_require_permission $survey_id survsimp_take_survey

If the user has not been assigned the survsimp_take_survey privilege, he or she will not be able to proceed. A slightly more complicated case is seen in survey-simple/www/admin/question-modify-text.tcl:

ad_require_permission $question_id survsimp_modify_question

Here, the user will not be able to proceed unless he or she has the survsimp_modify_question granted on this particular question. Rather than grant privileges on every single question in a survey, the context_id parameter of the survsimp_question.new parameter is used to allow privileges granted to a user regarding the survey, to also apply to the questions. When a question is created by a call to survsimp_question.new the context_id is set to the survey_id of the survey of which the question is a member. Any user who has privileges over the survey, automatically has the same privileges over the questions. Thus, if a user is granted the survsimp_admin_survey privilege over the survey, he or she will have survsimp_admin_survey privilege, and hence the survsimp_modify_question privilege over all the questions of which that survey is comprised.

Another case is when we simply want to control access to a page rather than a particular object. For example, only users with the survsimp_admin_survey privilege should be permitted to access survey-simple/www/admin/index.tcl. In this case, we use:

ad_require_permission [ad_conn package_id] survsimp_admin_survey

This checks if the user has the correct privilege for the package, which itself is simply and instance of the acs_object type.

Finally, privileges can be used in SQL statements to restrict the rows retrieved. For example, not all users have permission to take all surveys. In survey-simple/www/index.tcl, we present the user with a list of the surveys he or she is permitted to take using the following:

db_multirow surveys survey_select {
    select survey_id, name
    from survsimp_surveys, acs_objects
    where object_id = survey_id
    and context_id = :package_id
    and acs_permission.permission_p(object_id, :user_id, 'survsimp_take_survey') = 't'
    and enabled_p = 't'
    order by upper(name)
}

nstrug@arsdigita.com