0.00%
Search · Index

Weblog Page

Filtered by date 2008-02-17, 11 - 20 of 111 Postings (all, summary)

Upgrading Platform components

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

OpenACS Full Text Search requires several pieces: the OpenFTS code, some database functions, and the OpenFTS Engine. This section describes how to upgrade OpenFTS from 0.2 to 0.3.2 and upgrade the search engine on an OpenACS site at the same time.

  1. Uninstall the old OpenFTS Engine from the $OPENACS_SERVICE_NAME database.

    1. Browse to http://yourserver/openfts.

    2. Click Administration.

    3. Click Drop OpenFTS Engine

  2. Build and install the new OpenFTS driver and supporting tcl procedures. (This section of shell code is not fully documented; please exercise care.)

    cd /usr/local/src/
              tar xzf /var/tmp/Search-OpenFTS-tcl-0.3.2.tar.gz
              chown -R root.root Search-OpenFTS-tcl-0.3.2/
              cd Search-OpenFTS-tcl-0.3.2/
              ./configure --with-aolserver-src=/usr/local/src/aolserver/aolserver --with-tcl=/usr/lib/
              cd aolserver/
              make
              

    Back up the old fts driver as a precaution and install the newly compiled one

    mv /usr/local/aolserver/bin/nsfts.so /usr/local/aolserver/bin/nsfts-0.2.so
              cp nsfts.so /usr/local/aolserver/bin
              

    Build and install the OpenFTS code for PostGresSQL

    cd /usr/local/src/Search-OpenFTS-tcl-0.3.2/
              cp -r pgsql_contrib_openfts /usr/local/src/postgresql-7.2.3/contrib /usr/local/src/postgresql-7.2.3/contrib/pgsql_contrib_openfts
              make
              su - postgres
              cd tsearch/
              make
              make install
              exit

    In order for the OpenACS 4.6 OpenFTS Engine to use the OpenFTS 0.3.2 driver, we need some commands added to the database.

    [root root]# su - $OPENACS_SERVICE_NAME
    
              [$OPENACS_SERVICE_NAME dev]$ psql $OPENACS_SERVICE_NAME -f /usr/local/pgsql/share/contrib/openfts.sql
              CREATE
              CREATE
              [$OPENACS_SERVICE_NAME dev]$ psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.2.3/contrib/tsearch/tsearch.sql
              BEGIN
              CREATE
              (~30 more lines)
              [$OPENACS_SERVICE_NAME dev]$ exit
              [root root]#
              su - $OPENACS_SERVICE_NAME
    psql $OPENACS_SERVICE_NAME -f /usr/local/pgsql/share/contrib/openfts.sql
    psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.2.3/contrib/tsearch/tsearch.sql
    exit
    
  3. OPTIONAL: Install the new OpenFTS Engine.If you want to upgrade the OpenFTS Engine, do these steps. (You must have already upgraded the OpenFTS driver to 0.3.2.)

    1. Browse to http://yourserver/admin/site-map

    2. On the openfts line, click on set parameters.

    3. Change the value of openfts_tcl_src_path from /usr/local/src/Search-OpenFTS-tcl-0.2/ to /usr/local/src/Search-OpenFTS-tcl-0.3.2/

    4. Click Set Parameters

    5. [root root]# restart-aolserver $OPENACS_SERVICE_NAME
      
    6. Browse to http://yourserver/openfts

    7. Click Administration.

    8. Click Initialize OpenFTS Engine

An OpenACS database created in PostGreSQL 7.2 will not work correctly in PostGreSQL 7.3. This is because 7.2 truncates function names to 31 characters, but 7.3 does not. This does not cause problems in 7.2, because truncation occurs both at function creation and at function calling, so they still match. But if you use a database created in 7.2 in 7.3, the function names in the database remain truncated but the function calls are not, and so they don't match. Also some functions use casting commands that no longer work in 7.3 and these functions must be recreated.

To upgrade an OpenACS site from PostGreSQL 7.2 to 7.3, first upgrade the kernel to 4.6.3. Then, dump the database, run the upgrade script /var/lib/aolserver/$OPENACS_SERVICE_NAME/bin/pg_7.2to7.3_upgrade_helper.pl on the dump file, and reply the dump. See Forum OpenACS Q&A: PG 7.2->7.3 upgrade gotcha?. Example:

  1. Back up the database as per PostgreSQL.

  2. Run the upgrade script on the backup file.

    [root root]# su - $OPENACS_SERVICE_NAME
    
              [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]# cd /var/lib/aolserver/$OPENACS_SERVICE_NAME/bin
              [$OPENACS_SERVICE_NAME bin]$ ./pg_7.2to7.3_upgrade_helper.pl  ../database-backup/nightly.dmp  ../database-backup/upgrade-7.3.dmp  /var/lib/aolserver/$OPENACS_SERVICE_NAME
    
              ==================================================================
              looking for function acs_object__check_object_ancest in oacs
              grep result: /var/lib/aolserver/aufrecht-dev/packages/acs-kernel/sql/postgresql/acs-objects-create.sql:create function acs_object__check_object_ancestors (integer,integer,integer)
    
              replacing acs_object__check_object_ancest with acs_object__check_object_ancestors
    
              (many lines omitted)
              [$OPENACS_SERVICE_NAME bin]$
              
  3. Use perl to replace timestamp with timestamptz in the dump file. See example perl code in step two in /contrib/misc/upgrade_4.6_to_5.0.sh

  4. Create a new user for PostgreSQL 7.3.x, as per the Postgres installation guide. Keep in mind that your installation location is different, and your startup script (/etc/init.d/postgres73 should be named differently. You might even need to edit that file to make the paths correct). You'll also need to add export PGPORT=5434 to the .bashrc and/or .bash_profile for the postgres73 user.

  5. Install PostgreSQL 7.3.x. Note that you PostgreSQL must listen on a different port in order to work correctly, so you'll need to edit the configuration file (/usr/local/pgsql73/data/postgresql.conf) and change the port (to 5433, say). create a second postgres user to differentiate between the two postgres installs. When you do ./configure, you'll need to include --prefix=$HOME to ensure that it is installed in the postgres73 user's home directory.

  6. Change the path in $OPENACS_SERVICE_NAME's .bashrc or .bash_profile (or both) files to reflect the new postgres73 user directory. Also add in the PGPORT.

  7. Restore the database from dump as per the recovery instructions.

Install Full Text Search

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Joel Aufrecht and Malte Sussdorff

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

If you want full text search, and you are running PostgreSQL, install this module to support FTS. Do this step after you have installed both PostgreSQL and AOLserver. You will need the openfts tarball in /var/tmp.

  1. Install Tsearch. This is a PostgreSQL module that OpenFTS requires.

    [root root]# su - postgres
    [postgres pgsql]$ cd /usr/local/src/postgresql-7.3.4/contrib/tsearch/
    [postgres tsearch]$ make
    sed 's,MODULE_PATHNAME,$libdir/tsearch,g' tsearch.sql.in >tsearch.sql
    /usr/bin/flex  -8 -Ptsearch_yy -o'parser.c' parser.l(many lines omitted)
    rm -f libtsearch.so
    ln -s libtsearch.so.0.0 libtsearch.so
    [postgres tsearch]$ make install
    mkdir /usr/local/pgsql/share/contrib
    mkdir /usr/local/pgsql/doc/contrib
    (2 lines omitted)
    /bin/sh ../../config/install-sh -c -m 755 libtsearch.so.0.0 /usr/local/pgsql/lib/tsearch.so
    [postgres tsearch]$ exit
    logout
    
    [root root]#
    su - postgres
    cd /usr/local/src/postgresql-7.3.4/contrib/tsearch
    make
    make install
    exit
    
  2. Unpack the OpenFTS tarball and compile and install the driver.

    [root root]# cd /usr/local/src
    [root src]# tar xzf /var/tmp/Search-OpenFTS-tcl-0.3.2.tar.gz
    [root src]# cd /usr/local/src/Search-OpenFTS-tcl-0.3.2/
    [root Search-OpenFTS-tcl-0.3.2]# ./configure --with-aolserver-src=/usr/local/src/aolserver/aolserver --with-tcl=/usr/lib/
    checking prefix... /usr/local
    checking for gcc... gcc
    (many lines omitted)
    configure: creating ./config.status
    config.status: creating Makefile.global
    [root Search-OpenFTS-tcl-0.3.2]# make
    (cd parser; make all)
    make[1]: Entering directory `/usr/local/src/Search-OpenFTS-tcl-0.3.2/parser'
    (many lines omitted)
    packages provided were {Lingua::Stem::Snowball 0.3.2}
    processed fts_base_snowball.tcl
    [root Search-OpenFTS-tcl-0.3.2]# cd aolserver
    [root aolserver]# make
    gcc -c -fPIC  -DPACKAGE=\"OPENFTS\" -DVERSION=\"0.3.2\" -DHAVE_UNISTD_H=1 -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STR
    (many lines omitted)
    n_stem.o italian_stem.o norwegian_stem.o portuguese_stem.o russian_stem.o nsfts.o  -o nsfts.so
    [root aolserver]# cp nsfts.so /usr/local/aolserver/bin/
    [root aolserver]#
    cd /usr/local/src
    tar xzf /var/tmp/Search-OpenFTS-tcl-0.3.2.tar.gz
    cd /usr/local/src/Search-OpenFTS-tcl-0.3.2/
    ./configure --with-aolserver-src=/usr/local/src/aolserver/aolserver --with-tcl=/usr/lib/
    make
    cd aolserver
    make
    cp nsfts.so /usr/local/aolserver/bin
    
    
  3. Build some supplemental modules.

    [root aolserver]# cd /usr/local/src/Search-OpenFTS-tcl-0.3.2
    [root Search-OpenFTS-tcl-0.3.2]# cp -r pgsql_contrib_openfts /usr/local/src/postgresql-7.3.4/contrib
    [root Search-OpenFTS-tcl-0.3.2]# cd /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts
    [root pgsql_contrib_openfts]# make
    sed 's,MODULE_PATHNAME,$libdir/openfts,g' openfts.sql.in >openfts.sql
    gcc -O2 -Wall -Wmissing-prototypes -Wmissing-declarations -fpic -I. -I../../src/include   -c -o openfts.o openfts.c
    gcc -shared -o openfts.so openfts.o
    rm openfts.o
    [root pgsql_contrib_openfts]# su postgres
    [postgres pgsql_contrib_openfts]$ make install
    /bin/sh ../../config/install-sh -c -m 644 openfts.sql /usr/local/pgsql/share/contrib
    /bin/sh ../../config/install-sh -c -m 755 openfts.so /usr/local/pgsql/lib
    /bin/sh ../../config/install-sh -c -m 644 ./README.openfts /usr/local/pgsql/doc/contrib
    [postgres pgsql_contrib_openfts]$ exit
    [root pgsql_contrib_openfts]#
    cd /usr/local/src/Search-OpenFTS-tcl-0.3.2
    cp -r pgsql_contrib_openfts /usr/local/src/postgresql-7.3.4/contrib
    cd /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts
    make
    su postgres
    make install
    exit
    

If you are installing Full Text Search, add required packages to the new database. (In order for full text search to work, you must also install the PostgreSQL OpenFTS module and prerequisites.)

[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ /usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/tsearch/tsearch.sql
BEGIN
CREATE
(many lines omitted)
INSERT 0 1
COMMIT
[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ /usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts/openfts.sql
CREATE
CREATE
[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$
/usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/tsearch/tsearch.sql
/usr/local/pgsql/bin/psql $OPENACS_SERVICE_NAME -f /usr/local/src/postgresql-7.3.4/contrib/pgsql_contrib_openfts/openfts.sql

Note

If you get the error ERROR: could not access file "$libdir/tsearch": no such file or directory It is probably because PostgreSQL's libdir configuration variable points to a diffent directory than where tsearch is. You can find out where PostgreSQL expects to find tsearch via

pg_config --pkglibdir

If you have installed OpenFTS, you can enable it for this service. Uncomment this line from config.tcl. (To uncomment a line in a tcl file, remove the # at the beginning of the line.)

#ns_param   nsfts           ${bindir}/nsfts.so
  1. Click Admin on the top of the default home page. If prompted, log in with the account and password you entered during install.

  2. Click on the Install software link.

  3. Click on the Install new service link.

  4. Click on the Install link next to OpenFTS Driver.

  5. Restart the service.

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ svc -t /service/$OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$
  6. Wait a minute, then browse back to the home page.

  7. Click on Admin on the top of the screen.

  8. Click on Main Site Administration in the "Subsite Administration" section.

  9. Click on Site Map in the "Advanced Features" section.

  10. Mount the OpenFTS Full Text Search Engine in the site map.

    1. Click the new sub folder link on the "/" line, the first line under Main Site:/.

    2. Type openfts and click New.

    3. On the new openfts line, click the mount link.

    4. Click OpenFTS Driver.

    5. On the openfts line, click set parameters.

    6. Change openfts_tcl_src_path to /usr/local/src/Search-OpenFTS-tcl-0.3.2/ and click Set Parameters

  11. Mount the Search interface in the site map.

    1. Click the new sub folder link on the Main Site line.

    2. Type search and click New.

    3. Click the new application link on the search line.

    4. Type search where it says untitled, choose search from the drop-down list, and click New.

  12. Restart the service.

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ svc -t /service/$OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$
  13. Wait a minute, then click on Main Site at the top of the page.

  14. Initialize the OpenFTS Engine. This creates a set of tables in the database to support FTS.

    Near the bottom of the page, click on the OpenFTS Driver link. Click on Administration. Click on Initialize OpenFTS Engine. Click Initialize OpenFTS Engine.

  15. Add the FTS Engine service contract

    1. Click on the DevAdmin.

    2. Click on the Service Contract link.

    3. On the FtsEngineDriver line, click Install.

  16. Restart the service.

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ svc -t /service/$OPENACS_SERVICE_NAME
    
    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$

Enabling Full Text Search in packages at the moment is not trivial. It involves a couple of steps, which I will illustrate taking lars-blogger as an example package

  1. Install the package.

    1. Click Admin on the top of the default home page. If prompted, log in with the account and password you entered during install.

    2. Click on the Install software link.

    3. Click on the Install new application link.

    4. Click on the Install link next to Weblogger.

    5. Install all required packages as well (always say okay until you shall restart the server)

  2. Load the service contracts datamodell and enable the service contract

    [$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ cd packages/lars-blogger/sql/postgresql
    [$OPENACS_SERVICE_NAME postgresql]$ psql $OPENACS_SERVICE_NAME -f lars-blogger-sc-create.sql

    Note: Usually this script is called package_name-sc-create.sql

  3. Restart the service.

    [$OPENACS_SERVICE_NAME postgresql]$ svc -t /service/$OPENACS_SERVICE_NAME
    
                    [$OPENACS_SERVICE_NAME postgresl]$

If you are lucky, Full Text Search is enabled now, if not consult http://openacs.org/forums/message-view?message_id=154759. This link also contains some hints on how to make sure it is enabled.

Package Manager Design

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Bryan Quinn

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

In general terms, a package is a unit of software that serves a single well-defined purpose. That purpose may be to provide a service directly to one or more classes of end-user, (e.g., discussion forums and file storage for community members, user profiling tools for the site publisher), or it may be to act as a building block for other packages (e.g., an application programming interface (API) for storing and querying access control rules, or an API for scheduling email alerts). Thus, packages fall into one of two categories:

An installation of the OpenACS includes the OpenACS Kernel, some services that extend the kernel's functionality, and some applications intended for end-users. Packages function as individual pieces of subsites. A subsite can contain multiple application and service instances that provide the end-user with capabilities and content customized to the particular subsite.

This architecture supports the growth of collaborative commerce. For example, Jane User starts a forum focusing on the merits of View Cameras by creating an instance of the Forum application for her personal subsite on an OpenACS Installation. Jack User discovers Jane's forum and includes a link to it in his subsite. As interest in Jane's forum grows, she creates a subsite specializing in providing information about View cameras. This subsite now includes several package instances beyond Forum; it could potentially include its own Ecommerce capabilities (ala Yahoo! Shopping). This could include a knowledge management application that allows users to spread expertise about view cameras and a portal application that links to reliable camera models and resellers. Any subsite enabled package that is added to the OpenACS installation through APM is another potential package instance that can become part of Jane's View Camera subsite.

The APM provides an architecture for packaging software, making instances of that software available to subsites, specifying configuration parameters for each instance, and managing the creation and release of new packages.

Prior to ACS 3.3, all packages were lumped together into one monolithic distribution without explicit boundaries; the only way to ascertain what comprised a given package was to look at the top of the corresponding documentation page, where, by convention, the package developer would specify where to find:

  • the data model

  • the Tcl procedures

  • the user-accessible pages

  • the administration pages

Experience has shown us that this lack of explicit boundaries causes a number of maintainability problems for pre-3.3 installations:

  1. Package interfaces were not guaranteed to be stable in any formal way, so a change in the interface of one package would often break dependent packages (which we would only discover through manual regression testing). In this context, any of the following could constitute an interface change:

    • renaming a file or directory that appears in a URL

    • changing what form variables are expected as input by a page

    • changing a procedural abstraction, e.g., a PL/SQL or Java stored procedure or a Tcl procedure

    • changing a functional abstraction, e.g., a database view or a PL/SQL or Java stored function

    • changing the data model

    This last point is especially important. In most cases, changing the data model should not affect dependent packages. Rather, the package interface should provide a level of abstraction above the data model (as well as the rest of the package implementation). Then, users of the package can take advantage of implementation improvements that don't affect the interface (e.g., faster performance from intelligent denormalization of the data model), without having to worry that code outside the package will now break.

  2. A typical ACS-backed site only uses a few of the modules included in the distribution, yet there was no well-understood way to pick only what you needed when installing the ACS, or even to uninstall what you didn't need, post-installation. Unwanted code had to be removed manually.

  3. Releasing a new version of the ACS was complicated, owing again to the monolithic nature of the software. Since we released everything in the ACS together, all threads of ACS development had to converge on a single deadline, after which we would undertake a focused QA effort whose scale increased in direct proportion to the expansion of the ACS codebase.

  4. There was no standard way for developers outside of ArsDigita to extend the ACS with their own packages. Along the same lines, ArsDigita programmers working on client projects had no standard way to keep custom development cleanly separated from ACS code. Consequently, upgrading an already installed ACS was an error-prone and time-consuming process.

Consistent use of the APM format and tools will go a long way toward solving the maintainability problems listed above. Moreover, APM is the substrate that will enable us to establish a central package repository, where developers will be able publish their packages for other OpenACS users to download and install.

For a simple illustration of the difference between ACS without APM (pre-3.3) and ACS with APM (3.3 and beyond), consider a hypothetical ACS installation that uses only two of the thirty-odd modules available circa ACS 3.2 (say, bboard and e-commerce):

APM itself is part of a package, the OpenACS Kernel, an OpenACS service that is the only mandatory component of an OpenACS installation.

The OpenACS is a platform for web-based application software, and any software platform has the potential to develop problems like those described above. Fortunately, there are many precedents for systematic solutions, including:

Borrowing from all of the above, OpenACS 3.3 introduces its own package management system, the OpenACS Package Manager (APM), which consists of:

  • a standard format for APM packages (also called "OpenACS packages"), including:

    • version numbering, independent of any other package and the OpenACS as a whole

    • specification of the package interface

    • specification of dependencies on other packages (if any)

    • attribution (who wrote it) and ownership (who maintains it)

  • web-based tools for package management:

    • obtaining packages from a remote distribution point

    • installing packages, if and only if:

      1. all prerequisite packages are installed

      2. no conflicts will be created by the installation

    • configuring packages (obsoleting the monolithic OpenACS configuration file)

    • upgrading packages, without clobbering local modifications

    • uninstalling unwanted packages

  • a registry of installed packages, database-backed and integrated with filesystem-based version control

  • web-based tools for package development:

    • creating new packages locally

    • releasing new versions of locally-created packages

The design chosen for APM was meant to satisfy the following constraints:

  • The process of authoring a package must be as simple as possible.

  • Strict conventions must be established that provide a set of canonical locations and names for files and patterns, for OpenACS application development.

  • The processes of installing, upgrading, and using packages must be straightforward and accessible through a web-based UI.

  • Package instances must be able to have subsite-specific content available at an easily configurable URL.

All of these requirements were met, but at the cost of development simplicity. As Packages demonstrates, a set of strict directory conventions are required in order for a package to use APM. This contrasts with the apparent simplicity available to developers of the OpenACS 3.3 system. However, while the system has become more complex for developers to build packages, this complexity is easily managed and is compensated for by additional capabilities.

For example, to make a new application available to the system, a developer must:

  1. Create the necessary files to support the data model, Tcl API, and UI pages.

  2. Put the files in the correct locations for APM to be aware of them.

  3. Use APM to create a new package and enable it.

  4. Use the Site Map facility to create an instance of the package, mount it on an appropriate URL, and set parameters for that particular instance.

While this is complex, especially to a new OpenACS developer, the documentation walks the developer through each of these steps. Moreover, from following these steps, the package can be subsite specific, available to subsites across the system, and be available for distribution to other OpenACS installations without doing a monolithic upgrade or reinstall.

The APM is composed of systems for accomplishing a set of package-related tasks. Each of these tasks comprise a feature area that has an API, data model, and a UI:

  • Authoring a Package

  • Maintaining Multiple Versions of a Package

  • Creating Instances of the Package

  • Specifying Configuration Parameters for each Instance

Authoring a Package

Full instructions on how to prepare an OpenACS package are available in Packages. The API here can be invoked manually by a package's data model creation script, but need not to be used. This API is part of the APM PL/SQL package.

-- Informs the APM that this application is available for use.
procedure register_application (
    package_key         in apm_package_types.package_key%TYPE,
    pretty_name         in apm_package_types.pretty_name%TYPE,
    pretty_plural       in apm_package_types.pretty_plural%TYPE,
    package_uri         in apm_package_types.package_uri%TYPE,
    singleton_p         in apm_package_types.singleton_p%TYPE
                                default 'f',
    spec_file_path      in apm_package_types.spec_file_path%TYPE
                                default null,
    spec_file_mtime     in apm_package_types.spec_file_mtime%TYPE
                                default null
);

The procedure above registers an OpenACS application in the APM. It creates a new OpenACS object and stores information about the package, such as its name, in the APM data model. There is an analogous procedure for OpenACS services, called apm.register_service.

To remove an application from the system, there are the calls apm.unregister_application and apm.unregister_service.

-- Remove the application from the system.
procedure unregister_application (
    package_key     in apm_package_types.package_key%TYPE,
    -- Delete all objects associated with this application.
    cascade_p       in char default 'f'
);

Use the cascade_p only if you want to completely remove the package from the OpenACS.

In order to determine if a particular package exists in the system, use the register_p predicate. It returns 1 if the specified package_key exists in the system, 0 otherwise.

function register_p (
    package_key     in apm_package_types.package_key%TYPE
) return integer;

Maintaining Multiple Versions of a Package

While the package authoring API provides a means for registering a package, some information about a package is version dependent. For example, between versions, the owner of a package, its vendor, its URI, and its dependency information may change. The API for package versions allows this information to be specified. All of these APIs are part of the apm_package_version PL/SQL package.

To create a new package version, use the apm_package_version.new constructor function.

function new (
    version_id          in apm_package_versions.version_id%TYPE
                default null,
    package_key         in apm_package_versions.package_key%TYPE,
    version_name        in apm_package_versions.version_name%TYPE
                                default null,
    version_uri         in apm_package_versions.version_uri%TYPE,
    summary         in apm_package_versions.summary%TYPE,
    description_format      in apm_package_versions.description_format%TYPE,
    description         in apm_package_versions.description%TYPE,
    release_date        in apm_package_versions.release_date%TYPE,
    vendor          in apm_package_versions.vendor%TYPE,
    vendor_uri          in apm_package_versions.vendor_uri%TYPE,
    installed_p         in apm_package_versions.installed_p%TYPE
                                default 'f',
    data_model_loaded_p     in apm_package_versions.data_model_loaded_p%TYPE
                        default 'f'
) return apm_package_versions.version_id%TYPE;

In order to use this function, an existing package_key must be specified. The version_name parameter must follow a strict convention:

  1. A major version number

  2. at least one minor version number. Although any number of minor version numbers may be included, three minor version numbers is sufficient and is the convention of software developers.

  3. One of the following:

    • The letter d, indicating a development-only version

    • The letter a, indicating an alpha release

    • The letter b, indicating a beta release

    • No letter at all, indicating a final production release

In addition, the letters d, a, and b may be followed by another integer, indicating a version within the release.

For those who like regular expressions:

version_number := ^[0-9]+((\.[0-9]+)+((d|a|b|)[0-9]?)?)$

So the following is a valid progression for version numbers:

0.9d, 0.9d1, 0.9a1, 0.9b1, 0.9b2, 0.9, 1.0, 1.0.1, 1.1b1, 1.1

To delete a given version of a package, use the apm_package_version.delete procedure:

procedure delete (
    package_id      in apm_packages.package_id%TYPE
);

After creating a version, it is possible to edit the information associated with it using apm_package_version.edit.

function edit (
      new_version_id        in apm_package_versions.version_id%TYPE
                default null,
      version_id        in apm_package_versions.version_id%TYPE,
      version_name      in apm_package_versions.version_name%TYPE
                default null,
      version_uri       in apm_package_versions.version_uri%TYPE,
      summary           in apm_package_versions.summary%TYPE,
      description_format    in apm_package_versions.description_format%TYPE,
      description       in apm_package_versions.description%TYPE,
      release_date      in apm_package_versions.release_date%TYPE,
      vendor            in apm_package_versions.vendor%TYPE,
      vendor_uri        in apm_package_versions.vendor_uri%TYPE,
      installed_p       in apm_package_versions.installed_p%TYPE
                default 'f',
      data_model_loaded_p   in apm_package_versions.data_model_loaded_p%TYPE
                default 'f'
) return apm_package_versions.version_id%TYPE;

Versions can be enabled or disabled. Enabling a version instructs APM to source the package's libraries on startup and to make the package available to the OpenACS.

procedure enable (
    version_id          in apm_package_versions.version_id%TYPE
);

procedure disable (
    version_id          in apm_package_versions.version_id%TYPE
);

Files associated with a version can be added and removed. The path is relative to the package-root which is acs-server-root/packages/package-key.

-- Add a file to the indicated version.
function add_file(
    file_id             in apm_package_files.file_id%TYPE
                        default null,
    version_id in       apm_package_versions.version_id%TYPE,
    path                in apm_package_files.path%TYPE,
    file_type           in apm_package_file_types.file_type_key%TYPE
) return apm_package_files.file_id%TYPE;

-- Remove a file from the indicated version.
procedure remove_file(
    version_id          in apm_package_versions.version_id%TYPE,
    path                in apm_package_files.path%TYPE
);

Package versions need to indicate that they provide interfaces for other software. An interface is an API that other packages can access and utilize. Interfaces are identified as a URI and a version name, that comply with the specification of a version name for package URIs.

-- Add an interface provided by this version.
function add_interface(
    interface_id        in apm_package_dependencies.dependency_id%TYPE
                    default null,
    version_id          in apm_package_versions.version_id%TYPE,
    interface_uri       in apm_package_dependencies.service_uri%TYPE,
    interface_version       in apm_package_dependencies.service_version%TYPE
) return apm_package_dependencies.dependency_id%TYPE;

procedure remove_interface(
    interface_id        in apm_package_dependencies.dependency_id%TYPE,
    version_id          in apm_package_versions.version_id%TYPE
);

procedure remove_interface(
    interface_uri       in apm_package_dependencies.service_uri%TYPE,
    interface_version       in apm_package_dependencies.service_version%TYPE,
    version_id          in apm_package_versions.version_id%TYPE
);

The primary use of interfaces is for other packages to specify required interfaces, known as dependencies. A package cannot be correctly installed unless all of its dependencies have been satisfied.

-- Add a requirement for this version.  A requirement is some interface that this
-- version depends on.
function add_dependency(
    requirement_id      in apm_package_dependencies.dependency_id%TYPE
                    default null,
    version_id          in apm_package_versions.version_id%TYPE,
    requirement_uri     in apm_package_dependencies.service_uri%TYPE,
    requirement_version     in apm_package_dependencies.service_version%TYPE
) return apm_package_dependencies.dependency_id%TYPE;

procedure remove_dependency(
    requirement_id      in apm_package_dependencies.dependency_id%TYPE,
    version_id          in apm_package_versions.version_id%TYPE
);

procedure remove_dependency(
    requirement_uri     in apm_package_dependencies.service_uri%TYPE,
    requirement_version     in apm_package_dependencies.service_version%TYPE,
    version_id          in apm_package_versions.version_id%TYPE
);

As new versions of packages are created, it is necessary to compare their version names. These two functions assist in that task.

-- Given a version_name (e.g. 3.2a), return
-- something that can be lexicographically sorted.
function sortable_version_name (
    version_name        in apm_package_versions.version_name%TYPE
) return varchar;

-- Given two version names, return 1 if one > two, -1 if two > one, 0 otherwise.
-- Deprecate?
function compare(
    version_name_one        in apm_package_versions.version_name%TYPE,
    version_name_two        in apm_package_versions.version_name%TYPE
) return integer;

Creating Instances of a Package

Once a package is registered in the system, it is possible to create instances of it. Each instance can maintain its own content and parameters.

create or replace package apm_application
as

function new (
    application_id  in acs_objects.object_id%TYPE default null,
    instance_name   in apm_packages.instance_name%TYPE
            default null,
    package_key     in apm_package_types.package_key%TYPE,
    object_type     in acs_objects.object_type%TYPE
               default 'apm_application',
    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 (
    application_id      in acs_objects.object_id%TYPE
);
end apm_application;

Just creating a package instance is not sufficient for it to be served from the web server. A corresponding site node must be created for it. As an example, here is how the OpenACS API Documentation service makes itself available on the OpenACS main site:

declare
    api_doc_id integer;
begin
    api_doc_id := apm_service.new (
      instance_name => 'OpenACS API Browser',
      package_key => 'acs-api-browser',
      context_id => main_site_id
    );

    apm_package.enable(api_doc_id);

    api_doc_id := site_node.new (
      parent_id => site_node.node_id('/'),
      name => 'api-doc',
      directory_p => 't',
      pattern_p => 't',
      object_id => api_doc_id
    );

    commit;
end;
/
show errors


Specifying Configuration Parameters for each Instance

A parameter is a setting that can be changed on a package instance basis. Parameters are registered on each package_key, and the values are associated with each instance. Parameters can have default values and can be of type 'string' or 'number.' There is support with this API for setting a number of minimum and maximum values for each parameter, but for most instances, the minimum and maximum should be 1. It is useful to allow or require multiple values for packages that need to store multiple pieces of information under one parameter. Default values are automatically set when instances are created, but can be changed for each instance.

All of the functions below are in the APM PL/SQL package.

-- Indicate to APM that a parameter is available to the system.
function register_parameter (
    parameter_id        in apm_parameters.parameter_id%TYPE
                default null,
    parameter_name      in apm_parameters.parameter_name%TYPE,
    description         in apm_parameters.description%TYPE
                default null,
    package_key         in apm_parameters.package_key%TYPE,
    datatype            in apm_parameters.datatype%TYPE
                default 'string',
    default_value       in apm_parameters.default_value%TYPE
                default null,
    section_name        in apm_parameters.section_name%TYPE
                default null,
    min_n_values        in apm_parameters.min_n_values%TYPE
                default 1,
    max_n_values        in apm_parameters.max_n_values%TYPE
                default 1
) return apm_parameters.parameter_id%TYPE;

function update_parameter (
    parameter_id        in apm_parameters.parameter_id%TYPE,
    parameter_name      in apm_parameters.parameter_name%TYPE,
    description         in apm_parameters.description%TYPE
                default null,
    package_key         in apm_parameters.package_key%TYPE,
    datatype            in apm_parameters.datatype%TYPE
                default 'string',
    default_value       in apm_parameters.default_value%TYPE
                default null,
    section_name        in apm_parameters.section_name%TYPE
                default null,
    min_n_values        in apm_parameters.min_n_values%TYPE
                default 1,
    max_n_values        in apm_parameters.max_n_values%TYPE
                default 1
) return apm_parameters.parameter_name%TYPE;

-- Remove any uses of this parameter.
procedure unregister_parameter (
    parameter_id        in apm_parameters.parameter_id%TYPE
                default null
);

The following functions are used to associate values with parameters and instances:

-- Return the value of this parameter for a specific package and parameter.
function get_value (
    parameter_id        in apm_parameter_values.parameter_id%TYPE,
    package_id          in apm_packages.package_id%TYPE
) return apm_parameter_values.attr_value%TYPE;

function get_value (
    package_id          in apm_packages.package_id%TYPE,
    parameter_name      in apm_parameters.parameter_name%TYPE
) return apm_parameter_values.attr_value%TYPE;

-- Sets a value for a parameter for a package instance.
procedure set_value (
    parameter_id        in apm_parameter_values.parameter_id%TYPE,
    package_id          in apm_packages.package_id%TYPE,
    attr_value          in apm_parameter_values.attr_value%TYPE
);

procedure set_value (
    package_id          in apm_packages.package_id%TYPE,
    parameter_name      in apm_parameters.parameter_name%TYPE,
    attr_value          in apm_parameter_values.attr_value%TYPE
);

The central piece of the data model is the apm_package_types table where each package is registered. When a new application or service is installed on an OpenACS instance, a corresponding row in this table is inserted with information about the type of package, e.g. if the forum package is installed on your OpenACS server, a row in apm_package_types will be created, noting that it's an application package type.

The apm_packages table is used to contain information about the instances of packages currently created in the system. The package_key column references the apm_package_types table to ensure that no package instance can be created for a type that does not exist.

The apm_package_versions table contains information specific to a particular version of a package. Several tables reference this one to provide further information about the particular version:

  • apm_package_owners Stores information about the owners of a particular version of a package.

  • apm_package_files Stores information about the files that are part of a version.

  • apm_package_dependencies Stores information about what interfaces the package provides and requires.

Parameter information is maintained through two tables:

  • apm_parameters This table contains the definition of each of the parameters for a package.

  • apm_parameter_values This table holds all of the values of parameters for specific package instances.

A number of views are available for obtaining information about packages registered in the APM.

  • apm_package_version_info Provides information about all of the versions in the system with information available from the apm_package_types table.

  • apm_enabled_package_versions A view (subset) of the above table with only enabled versions.

  • apm_file_info Provides a public interface for querying file information.

The APM's user interface is part of the OpenACS Administration Service. The UI is the primary point of contact with APM by developers and administrators. It is part of OpenACS Administration, because only the site-wide administrator should be able to access it. Thus in order to develop a package, the developer must be granted site-wide administration.

APM has two parameters for configuring how it interacts with the UNIX filesystem, accessible via the Site Map admin page. These parameters need not be changed under most circumstances, but may need to be tweaked for Windows compatibility.

  • GzipExecutableDirectory This directory points to where the gunzip program can be found for uncompressing gzip archives. This is needed for the installation of .apm files which are simply gziped tarballs. Default is /usr/local/bin

  • InfoFilePermissionsMode This sets the default UNIX permissions used when creating files using the APM. Default is 775.

APM has been in production since OpenACS 3.3, and as of version 4.0 offers a stable set of features. One major feature planned is integration with the OpenACS Package Repository for automatic dependency satisfaction. When a user tries to install a package that depends on other packages, the APM will contact the package repository, determine what packages depend on it, and offer the user a chance to download and install them all. This improvement offers value to end users by facilitating the extension of their OpenACS systems.

Architecturally, minor improvements to the data model and the specification file are planned to increase modularity. The current implementation puts all package specification information in a single file. This approach has certain advantages, such as centralization, but splitting this information into several files allows for flexible extensions to the APM architecture over time.

APM packages currently lack provisions to verify security information. There are plans to add MD5 time stamps and PGP signatures to packages to enable secure authentication of packages. These steps are necessary for APM to be usable as a scalable method to distribute packages on multiple repositories worldwide.

Another anticipated change is to split the APM UI into separate systems for authoring, maintaining, and installing packages. The current UI presents all of this functionality in one interface and it can be confusing from a usability perspective.

  • System creator: Bryan Quinn, Jon Salz, Michael Yoon, Lars Pind, Todd Nightingale.

  • System owner: Bryan Quinn

  • Documentation author: Bryan Quinn, building from earlier versions by Jon Salz, Michael Yoon, and Lars Pind.

Document Revision # Action Taken, Notes When? By Whom?
0.1 Creation 9/25/2000 Bryan Quinn
0.8 Ready for QA 9/29/2000 Bryan Quinn
0.9 Edited for ACS 4 Beta release 10/02/2000 Kai Wu
1.0 Edited for OpenACS 4.5 Beta release 03/02/2002 Roberto Mello

OpenACS Data Models and the Object System

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Pete Su

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

Developing data models in OpenACS 5.2.3rc1 is much like developing data models for OpenACS 3, save for the implementation. As usual, you need to examine how to model the information that the application must store and manipulate, and define a suitable set of SQL tables. In our Notes application, we have to be able to keep track of who entered a particular note, when they did it, and the actual text of the notes that users have entered. A simple data model might look like this:

create table notes (
    note_id           integer primary key,
    owner_id          integer references users(user_id),
    creation_user     references(user_id) not null,
    creation_date     date not null,
    last_modified     date not null,
    title             varchar(255) not null,
    body              varchar(1024)
)

We've omitted constraint names for the purpose of clarity.

Thinking further ahead, we can imagine doing any of the following things with Notes as well:

  • Define access control policies on notes.

  • Attach user comments on notes.

  • Allow users to define custom fields to store on their notes.

  • Automatically generate input forms or output displays for notes.

  • Allow other applications to use notes in ways we don't know of yet.

In OpenACS, the key to enabling these types of services on your application data is to take advantage of the Object System. The first question, then, is "Just what are objects, and what do you use them for anyway?". The short answer: objects are anything represented in the application's data model that will need to be managed by any central service in OpenACS, or that may be reusable in the context of future applications. Every object in the system is represented using a row in the acs_objects table. This table defines all the standard attributes that are stored on every object, including its system-wide unique ID, object type, and some generic auditing columns.

To make use of the object system, you as the application developer have to write your data model in a way that is slightly more complex than in the ACS 3.x days. What you get for this extra work includes:

  • The Permissions System lets you track who is allowed to do what to the rows in an application table, and gives you an easy way to enforce this from Tcl.

  • Every object has an attribute called context_id that provides a way to trivially specify both the default permissions for an object, and the intended "scope" of an object. Just set the context_id to the controlling object and forget about it.

  • And most importantly, any future object-level service - from a general-comments replacement to personalized ranking - will become available to your application "for free."

Using ACS objects is straightforward: all that's required are a few extra steps in the design of your application data model.

In order to hook our Notes application into the object system, we make some calls to use our notes table as the basis for a new object type. Object types are analogous to classes in programming languages such as C++ and Java. In Java, a class defines a set of attributes that store data and a set of methods that run code. In OpenACS, we use one or more database tables to store the data attributes, and we define a stored procedure package to hold procedures to define the programming interface to the data model.

The object type itself is described using data in the acs_object_types and acs_attributes tables, which play a role similar to the data dictionary in Oracle. As in Java, object types can inherit attributes from a parent type, so the type system forms a hierarchy. Unlike Java, Oracle does not support this inheritance transparently, so we have to make sure we add our own bookkeeping code to keep everything consistent. Below you'll find the code needed to describe a new object type called notes in your system.

Fire up your text editor and open the ROOT/packages/notes/sql/oracle/notes-create.sql (ROOT/packages/notes/sql/postgresql/notes-create.sql for the PG version) file created when we created the package. Then, do the following:

First, add an entry to the acs_object_types table with the following PL/SQL call:

begin
  acs_object_type.create_type (
    supertype     => 'acs_object',
    object_type   => 'note',
    pretty_name   => 'Note',
    pretty_plural => 'Notes',
    table_name    => 'NOTES',
    id_column     => 'NOTE_ID'
  );
end;
/
show errors;

This PL/SQL call tells the system that we would like to use the table NOTES as the basis for a new object type called note. This type is a subtype of the acs_object type, which means that we want to inherit all of the basic attributes of all ACS objects. As mentioned, it will take some work on our part to make this happen, since Oracle can't do it automatically. In general, most basic applications will define types that are simple subtypes of acs_object.

Add entries to the acs_attributes table to describe the data attributes of the new type. This data can eventually be used to do things like automatically generate user interfaces to manipulate the notes table, though that functionality isn't yet available.

declare
 attr_id acs_attributes.attribute_id%TYPE;
begin
  attr_id := acs_attribute.create_attribute (
    object_type    => 'note',
    attribute_name => 'TITLE',
    pretty_name    => 'Title',
    pretty_plural  => 'Titles',
    datatype       => 'string'
  );

  attr_id := acs_attribute.create_attribute (
    object_type    => 'note',
    attribute_name => 'BODY',
    pretty_name    => 'Body',
    pretty_plural  => 'Bodies',
    datatype       => 'string'
  );
end;
/
show errors;

We can stop here and not bother to register the usual OpenACS 3.x attributes of creation_user, creation_date and last_modified, since the object type acs_object already defines these attributes. Again, because the new type note is a subtype of acs_object, it will inherit these attributes, so there is no need for us to define them.

The next thing we do is make a small modification to the data model to reflect the fact that each row in the notes table represents something that is not only an object of type note, but also an acs_object. The new table definition looks like this:

create table notes (
    note_id    integer references acs_objects(object_id) primary key,
    owner_id   integer references users(user_id),
    title      varchar(255) not null,
    body       varchar(1024)
)

The usual creation_date and modified_date columns are absent since they already exist in acs_objects. Also, note the constraint we have added to reference the acs_objects table, which makes clear that since note is a subtype of acs_object, every row in the notes table must have a corresponding row in the acs_objects table. This is the fundamental means by which we model inheritance; it guarantees that any services that use the acs_objects table to find objects will transparently find any objects that are instances of any subtype of acs_objects.

The next step is to define a PL/SQL package for your new type, and write some basic procedures to create and delete objects. Here is a package definition for our new type:

create or replace package note
as
  function new (
    note_id             in notes.note_id%TYPE default null,
    owner_id            in notes.owner_id%TYPE default null,
    title               in notes.title%TYPE,
    body                in notes.body%TYPE,
    object_type         in acs_object_types.object_type%TYPE default 'note',
    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 notes.note_id%TYPE;

  procedure delete (
    note_id      in notes.note_id%TYPE
  );
end note;
/
show errors

You might be wondering what all the extra parameters are to these calls, since we haven't mentioned them before. These parameters are needed to fill out information that will be stored about the object that's not stored directly in the table you defined. The OpenACS Object System defines these attributes on the type acs_object since all objects should have these attributes. Internally, there are tables that store this information for you. Most of the data is pretty self-explanatory and reflects attributes that existed in the earlier OpenACS 3.x data models, with the exception of the context_id attribute.

The context_id attribute stores the ID of an object that represents the default security domain to which the object belongs. It is used by the permissions system in this way: if no permissions are explicitly attached to the object, then the object inherits its permissions from the context. For example, if I had told you how to use the permissions system to specify that an object OBJ was "read only", then any other object that used OBJ as its context would also be "read only" by default. We'll talk about this more later.

The PL/SQL package body contains the implementations of the procedures defined above. The only subtle thing going on here is that we must use acs_object.new to insert a row into acs_objects, before inserting a row into the notes. Similarly, when we delete a row from note, we have to be sure to delete the corresponding acs_object row.

create or replace package body note
as

  function new (
    note_id             in notes.note_id%TYPE default null,
    owner_id            in notes.owner_id%TYPE default null,
    title               in notes.title%TYPE,
    body                in notes.body%TYPE,
    object_type         in acs_object_types.object_type%TYPE default 'note',
    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 notes.note_id%TYPE
  is
    v_note_id integer;
  begin
    v_note_id := acs_object.new (
      object_id     => note_id,
      object_type   => object_type,
      creation_date => creation_date,
      creation_user => creation_user,
      creation_ip   => creation_ip,
      context_id    => context_id
    );

    insert into notes
     (note_id, owner_id, title, body)
    values
     (v_note_id, owner_id, title, body);

     return v_note_id;
  end new;

  procedure delete (
    note_id      in notes.note_id%TYPE
  )
  is
  begin
    delete from notes
    where note_id = note.delete.note_id;

    acs_object.del(note_id);
  end delete;

end note;
/
show errors;

That's pretty much it! As long as you use the note.new function to create notes, and the note.delete function to delete them, you'll be assured that the relationship each note has with its corresponding acs_object is preserved.

The last thing to do is to make a file ROOT/packages/notes/sql/notes-drop.sql so it's easy to drop the data model when, say, you're testing:

begin
  acs_object_type.drop_type ('note');
end;
/
show errors

drop package note;
drop table notes;

While it is hard to give general design advice without knowing anything about a particular application, you should follow the following rule of thumb when deciding when to hook part of your data model to the object system:

Anything in your data model that needs to be available to general OpenACS services such as user comments, permissions, and so on should be a subtype of acs_object. In addition, if you want your data model to take advantage of attributes that exist in some object type that is a subtype of acs_object, then you should use the object system.

For example, for most applications, you will want to use objects to represent the data in your application that is user visible and thus requires access control. But other internal tables, views, mapping tables and so on probably don't need to be objects. As before, this kind of design decision is mostly made on an application-by-application basis, but this is a good baseline from which to start.

In this section we cover some overall guidelines for designing data models that are meant to be integrated with the OpenACS object system.

There are two basic rules you should follow when designing OpenACS 5.2.3rc1 data models:

  1. Never utilize fields in the acs_objects table in application specific ways. That is, never assign any application-specific semantics to this data. In the notes application, we use the creation_date and last_modified fields, but this is OK since we do not assign any application-specific meaning to these fields.

  2. In particular, never assign any application specific semantics to the context_id attribute of an object. This field is used for a very specific purpose by the permissions system, and using this field in any other way whatsoever is guaranteed to make your application act strangely.

    As we'll see later, the Notes example will point each note object's context_id to the package instance in which the note was created. The idea will be that in a real site, the administrator would create one package instance for every separate set of Notes (say, one per user). The instance would "own" all of the notes that it created, and the administrator would be able to use the package instance as the basis for access control, which is convenient.

The reason behind these two rules is pretty straightforward: First, the OpenACS Object system itself is meant to be a generic and reusable tool for any application to use for basic services. Second, in order for this to work, the various parts of the OpenACS Objects data model must be interpreted in the same way by all applications that use the data model. Therefore, assigning any application-specific semantics to any part of the core data model is a bad thing to do, because then the semantics of the data model are no longer independent of the application. This would make it impossible to build the generic tools that the data model is trying to support.

Another less important reason for these two rules is to not introduce any joins against the acs_objects table in SQL queries in your application that you do not absolutely need.

In the Notes example, the result of applying these rules is that we are careful to define our own attribute for owner_id rather than overloading creation_user from the objects table. But, since we will probably use creation_date and so on for their intended purposes, we don't bother to define our own attributes to store that data again. This will entail joins with acs_objects but that's OK because it makes the overall data model cleaner. The real lesson is that deciding exactly how and when to use inherited attributes is fairly straightforward, but requires a good amount of thought at design time even for simple applications.

Hooking into the OpenACS 5.2.3rc1 object system brings the application developer numerous benefits, and doing it involves only four easy steps:

  • Describe the a new object type to the system. Most new application types will be subtypes of the built-in type acs_object.

  • Define a table to store application object data.

  • Define a PL/SQL package to store procedures related to the new type. You have to define at least a function called new to create new application objects and a procedure called delete to delete them.

  • Define a package body that contains the implementations of the PL/SQL procedures defined above.

  • Try not to write queries in your application that join against acs_objects. This means you should never use the fields in acs_objects for application-specific purposes. This is especially true for the context_id field.

Admin Pages

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

There are at least two flavors of admin user interface:

  • Admins use same pages as all other users, except that they are offered admin links and buttons where appropriate. For example, if admins have privilege to bulk-delete items you could provide checkboxes next to every item seen on a list and the Delete Selected button on the bottom of the list.

  • Dedicated admin pages. If you want admins to have access to data that users aren't interested in or aren't allowed to see you will need dedicated admin pages. The conventional place to put those dedicated admin pages is in the /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/www/admin directory.

    [$OPENACS_SERVICE_NAME www]$ mkdir admin
    
    [$OPENACS_SERVICE_NAME www]$ cd admin
    

    Even if your application doesn't need any admin pages of its own you will usually need at least one simple page with a bunch of links to existing administration UI such as Category Management or standard Parameters UI. Adding the link to Category Management is described in the section on categories. The listing below adds a link to the Parameters UI of our package.

    [$OPENACS_SERVICE_NAME admin]$ vi index.adp
    
    <master>
    <property name="title">@title;noquote@</property>
    <property name="context">@context;noquote@</property>
    
    <ul class="action-links">
      <li><a href="@parameters_url@" title="Set parameters" class="action_link">Set parameters</a></li>
    </ul>
    
    [$OPENACS_SERVICE_NAME admin]$ vi index.tcl
    
    ad_page_contract {} {
    } -properties {
        context_bar
    }
    
    set package_id [ad_conn package_id]
    
    permission::require_permission  -object_id $package_id  -privilege admin]
    
    set context [list]
    
    set title "Administration"
    
    set parameters_url [export_vars -base "/shared/parameters" {
      package_id { return_url [ad_return_url] }
    }]
    
    

    Now that you have the first admin page it would be nice to have a link to it somewhere in the system so that admins don't have to type in the /admin every time they need to reach it. You could put a static link to the toplevel index.adp but that might be distracting for people who are not admins. Besides, some people consider it impolite to first offer a link and then display a nasty "You don't have permission to access this page" message.

    In order to display the link to the admin page only to users that have admin privileges add the following code near the top of /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/www/admin/index.tcl:

    set package_id [ad_conn package_id]
    
    set admin_p [permission::permission_p -object_id $package_id  -privilege admin -party_id [ad_conn untrusted_user_id]]
    
    if { $admin_p } {
        set admin_url "admin"
        set admin_title Administration
    }
    

    In /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/www/admin/index.adp put:

    <if @admin_p@ ne nil>
      <a href="@admin_url@">@admin_title@</a>
    </if>
    

How to Update the translations

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

  1. Identify any new locales that have been created. For each new locale, check the parameters, especially that the locale is in the format [two-letter code for language, lower-case]_[TWO-LETTER CODE FOR COUNTRY, UPPER-CASE], and create a sql command. A example sql command for creating a locale is:

    insert into ad_locales
           (locale, label, language, country, nls_language, nls_territory,
            nls_charset, mime_charset, default_p, enabled_p)
           values ('fa_IR', 'Farsi (IR)', 'fa', 'IR', 'FARSI', 'IRAN', 'AL24UTFFSS',
            'windows-1256', 't', 'f');

    Put this command into the following four files. For the upgrade files, the correct file name will depend on the exact version.

    • /packages/acs-lang/sql/postgresql/ad-locales.sql

    • /packages/acs-lang/sql/postgresql/upgrade/upgrade-current-version.sql

    • /packages/acs-lang/sql/oracle/ad-locales.sql

    • /packages/acs-lang/sql/oracle/upgrade/upgrade-current-version.sql

  2. Make a backup of the production database. Restore it as a new database. For example, if upgrading from OpenACS 5.1.1, and the site name/database name is translate-511, create translate-512b1.

  3. Check out the latest code on the release branch (e.g., oacs-5-1) as a new site, using the new site name (e.g., /var/lib/aolserver/translate-512b1. Copy over any local settings - usually, /etc/config.tcl and /etc/daemontools/run and modify appropriately. Also, copy over several translation-server-only files:

    ...TBD
              

     

  4. Shut down the production site and put up a notice (no procedure on how to do this yet.)

  5. Start the new site, and upgrade it.

  6. Go to ACS Lang admin page and click "Import All Messages"

  7. Resolve conflicts, if any, on the provided page.

  8. Back on the admin page, click the export link. If there are conflicts, the messages will be exported anyway and any errors will be shown in the web interface.

  9. Commit the message catalogs to cvs.

  10. From the packages dir, run the acs-lang/bin/check-catalog.sh script. (This checks for keys no longer in use and some other things. Until it is rolled into the UI, do it manually and check the results and take whatever steps you can intuit you should do.)

  11. CVS commit the catalog files. Done

  12. If everything went well, reconfigure the new site to take over the role of the old site (/etc/config.tcl and /etc/daemontools/run). Otherwise, bring the old site back up while investigating problems, and then repeat.

Write the Requirements and Design Specs

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

Before you get started you should make yourself familiar with the tags that are used to write your documentation. For tips on editing SGML files in emacs, see the section called “OpenACS Documentation Guide”.

It's time to document. For the tutorial we'll use pre-written documentation. When creating a package from scratch, start by copying the documentation template from /var/lib/aolserver/openacs-dev/packages/acs-core-docs/xml/docs/xml/package-documentation-template.xml to myfirstpackage/www/docs/xml/index.xml.

You then edit that file with emacs to write the requirements and design sections, generate the html, and start coding. Store any supporting files, like page maps or schema diagrams, in the www/doc/xml directory, and store png or jpg versions of supporting files in the www/doc directory.

For this tutorial, you should instead install the pre-written documentation files for the tutorial app. Log in as $OPENACS_SERVICE_NAME, create the standard directories, and copy the prepared documentation:

[$OPENACS_SERVICE_NAME $OPENACS_SERVICE_NAME]$ cd /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/myfirstpackage/
[$OPENACS_SERVICE_NAME myfirstpackage]$ mkdir -p www/doc/xml
[$OPENACS_SERVICE_NAME myfirstpackage]$ cd www/doc/xml
[$OPENACS_SERVICE_NAME xml]$ cp /var/lib/aolserver/$OPENACS_SERVICE_NAME/packages/acs-core-docs/www/files/myfirstpackage/* .
[$OPENACS_SERVICE_NAME xml]$

OpenACS uses DocBook for documentation. DocBook is an XML standard for semantic markup of documentation. That means that the tags you use indicate meaning, not intended appearance. The style sheet will determine appearance. You will edit the text in an xml file, and then process the file into html for reading.

Open the file index.xml in emacs. Examine the file. Find the version history (look for the tag <revhistory>). Add a new record to the document version history. Look for the <authorgroup> tag and add yourself as a second author. Save and exit.

Process the xml file to create html documentation. The html documentation, including supporting files such as pictures, is stored in the www/docs/ directory. A Makefile is provided to generate html from the xml, and copy all of the supporting files. If Docbook is set up correctly, all you need to do is:

[$OPENACS_SERVICE_NAME xml]$ make
cd .. ; /usr/bin/xsltproc ../../../acs-core-docs/www/xml/openacs.xsl xml/index.xml
Writing requirements-introduction.html for chapter(requirements-introduction)
Writing requirements-overview.html for chapter(requirements-overview)
Writing requirements-cases.html for chapter(requirements-cases)
Writing sample-data.html for chapter(sample-data)
Writing requirements.html for chapter(requirements)
Writing design-data-model.html for chapter(design-data-model)
Writing design-ui.html for chapter(design-ui)
Writing design-config.html for chapter(design-config)
Writing design-future.html for chapter(design-future)
Writing filename.html for chapter(filename)
Writing user-guide.html for chapter(user-guide)
Writing admin-guide.html for chapter(admin-guide)
Writing bi01.html for bibliography
Writing index.html for book
[$OPENACS_SERVICE_NAME xml]$

Verify that the documentation was generated and reflects your changes by browsing to http://yoursite:8000/myfirstpackage/doc

Install LDAP for use as external authentication

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Malte Sussdorff

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.

This step by step guide on how to use LDAP for external authentication using the LDAP bind command, which differs from the approach usually taken by auth-ldap. Both will be dealt with in these section

  1. Install openldap.Download and install ns_ldap

    [root aolserver]# cd /usr/local/src/
              [root src]# wget ftp://ftp.openldap.org/pub/OpenLDAP/openldap-release/openldap-2.2.17.tgz
              [root src]# tar xvfz openldap-2.2.17.tgz
              [root src]# cd openldap-2.2.17
              [root src]# ./configure --prefix=/usr/local/openldap
              [root openldap]# make install
              [root openldap]#
    cd /usr/local/src/
    wget ftp://ftp.openldap.org/pub/OpenLDAP/openldap-release/openldap-2.2.17.tgz
    tar xvfz openldap-2.2.17.tgz
    cd openldap-2.2.17
    ./configure --prefix=/usr/local/openldap --disable-slapd
    make install
    
    
  2. Install ns_ldap.Download and install ns_ldap

    [root aolserver]# cd /usr/local/src/aolserver/
              [root aolserver]# wget http://www.sussdorff.de/ressources/nsldap.tgz
              [root aolserver]# tar xfz nsldap.tgz
              [root aolserver]# cd nsldap
              [root ns_pam-0.1]# make install LDAP=/usr/local/openldap INST=/usr/local/aolserver
              [root ns_pam-0.1]#
    cd /usr/local/src/aolserver/
    wget http://www.sussdorff.de/resources/nsldap.tgz
    tar xfz nsldap.tgz
    cd nsldap
    make install LDAP=/usr/local/openldap INST=/usr/local/aolserver
    
    
  3. Configure ns_ldap for traditional use.Traditionally OpenACS has supported ns_ldap for authentification by storing the OpenACS password in an encrypted field within the LDAP server called "userPassword". Furthermore a CN field was used for searching for the username, usually userID or something similar. This field is identical to the usernamestored in OpenACS. Therefore the login will only work if you change login method to make use of the username instead.

    • Change config.tcl. Remove the # in front of ns_param nsldap ${bindir}/nsldap.so to enable the loading of the ns_ldap module.

  4. Configure ns_ldap for use with LDAP bind.LDAP authentication usually is done by trying to bind (aka. login) a user with the LDAP server. The password of the user is not stored in any field of the LDAP server, but kept internally. The latest version of ns_ldap supports this method with the ns_ldap bind command. All you have to do to enable this is to configure auth_ldap to make use of the BIND authentification instead. Alternatively you can write a small script on how to calculate the username out of the given input (e.g. if the OpenACS username is malte.fb03.tu, the LDAP request can be translated into "ou=malte,ou=fb03,o=tu" (this example is encoded in auth_ldap and you just have to comment it out to make use of it).

Documenting Tcl Files: Page Contracts and Libraries

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Jon Salz on 3 July 2000

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.
  • Tcl procedures: /packages/acs-kernel/tcl-documentation-procs.tcl

In versions of the OpenACS prior to 3.4, the standard place to document Tcl files (both Tcl pages and Tcl library files) was in a comment at the top of the file:

#
# path from server home/filename
#
# Brief description of the file's purpose
#
# author's email address, file creation date
#
# $Id: tcl-doc.html,v 1.43 2006/07/17 05:38:32 torbenb Exp $
#

In addition, the inputs expected by a Tcl page (i.e., form variables) would be enumerated in a call to ad_page_variables, in effect, documenting the page's argument list.

The problem with these practices is that the documentation is only accessible by reading the source file itself. For this reason, ACS 3.4 introduces a new API for documenting Tcl files and, on top of that, a web-based user interface for browsing the documentation:

  • ad_page_contract: Every Tcl page has a contract that explicitly defines what inputs the page expects (with more precision than ad_page_variables) and incorporates metadata about the page (what used to live in the top-of-page comment). Like ad_page_variables, ad_page_contract also sets the specified variables in the context of the Tcl page.

  • ad_library: To be called at the top of every library file (i.e., all files in the /tcl/ directory under the server root and *-procs.tcl files under /packages/).

This has the following benefits:

  • Facilitates automatic generation of human-readable documentation.

  • Promotes security, by introducing a standard and automated way to check inputs to scripts for correctness.

  • Allows graphical designers to determine easily how to customize sites' UIs, e.g., what properties are available in templates.

  • Allows the request processor to be intelligent: a script can specify in its contract which type of abstract document it returns, and the request processor can transform it automatically into something useful to a particular user agent. (Don't worry about this for now - it's not complete for ACS 3.4.)

Currently ad_page_contract serves mostly as a replacement for ad_page_variables. Eventually, it will be integrated closely with the documents API so that each script's contract will document precisely the set of properties available to graphical designers in templates. (Document API integration is subject to change, so we don't decsribe it here yet; for now, you can just consider ad_page_contract a newer, better, documented ad_page_variables.)

Let's look at an example usage of ad_page_contract:

# /packages/acs-kernel/api-doc/www/package-view.tcl
ad_page_contract {
    version_id:integer
    public_p:optional
    kind
    { format "html" }
} {
    Shows APIs for a particular package.

    @param version_id the ID of the version whose API to view.
    @param public_p view only public APIs?
    @param kind view the type of API to view. One of <code>procs_files</code>,
        <code>procs</code>, <code>content</code>, <code>types</code>, or
        <code>gd</code>.
    @param format the format for the documentation. One of <code>html</code> or <code>xml</code>.

    @author Jon Salz (jsalz@mit.edu)
    @creation-date 3 Jul 2000
    @cvs-id $Id: tcl-doc.html,v 1.43 2006/07/17 05:38:32 torbenb Exp $
}

Note that:

  • By convention, ad_page_contract should be preceded by a comment line containing the file's path. The comment is on line 1, and the contract starts on line 2.

  • ad_page_contract's first argument is the list of expected arguments from the HTTP query (version_id, public_p, kind, and format). Like ad_page_variables, ad_page_contract sets the corresponding Tcl variables when the page is executed.

  • Arguments can have defaults, specified using the same syntax as in the Tcl proc (a two-element list where the first element is the parameter name and the second argument is the default value).

  • Arguments can have flags, specified by following the name of the query argument with a colon and one or more of the following strings (separated by commas):

    • optional: the query argument doesn't need to be provided; if it's not, the variable for that argument simply won't be set. For instance, if I call the script above without a public_p in the query, then in the page body [info exists public_p] will return 0.

    • integer: the argument must be an integer (ad_page_contract will fail and display and error if not). This flag, like the next, is intended to prevent clients from fudging query arguments to trick scripts into executing arbitrary SQL.

    • sql_identifier: the argument must be a SQL identifier (i.e., [string is wordchar $the_query_var] must return true).

    • trim: the argument will be [string trim]'ed.

    • multiple: the argument may be specified arbitrarily many times in the query string, and the variable will be set to a list of all those values (or an empty list if it's unspecified). This is analogous to the -multiple-list flag to ad_page_variables, and is useful for handling form input generated by <SELECT MULTIPLE> tags and checkboxes.

      For instance, if dest_user_id:multiple is specified in the contract, and the query string is

      ?dest_user_id=913&dest_user_id=891&dest_user_id=9
      
      

      then $dest_user_id is set to [list 913 891 9].

    • array: the argument may be specified arbitrarily many times in the query string, with parameter names with suffixes like _1, _2, _3, etc. The variable is set to a list of all those values (or an empty list if none are specified).

      For instance, if dest_user_id:array is specified in the contract, and the query string is

      ?dest_user_id_0=913&dest_user_id_1=891&dest_user_id_2=9
      
      

      then $dest_user_id is set to [list 913 891 9].

  • You can provide structured, HTML-formatted documentation for your contract. Note that format is derived heavily from Javadoc: a general description of the script's functionality, followed optionally by a series of named attributes tagged by at symbols (@). You are encouraged to provide:

    • A description of the functionality of the page. If the description contains more than one sentence, the first sentence should be a brief summary.

    • A @param tag for each allowable query argument. The format is

      @param parameter-namedescription...
      
    • An @author tag for each author. Specify the author's name, followed his or her email address in parentheses.

    • A @creation-date tag indicating when the script was first created.

    • A @cvs-id tag containing the page's CVS identification string. Just use $Id: tcl-documentation.html,v 1.2 2000/09/19 07:22:35 ron Exp $ when creating the file, and CVS will substitute an appropriate string when you check the file in.

    These @ tags are optional, but highly recommended!

ad_library provides a replacement for the informal documentation (described above) found at the beginning of every Tcl page. Instead of:

# /packages/acs-kernel/00-proc-procs.tcl
#
# Routines for defining procedures and libraries of procedures (-procs.tcl files).
#
# jsalz@mit.edu, 7 Jun 2000
#
# $Id: tcl-doc.html,v 1.43 2006/07/17 05:38:32 torbenb Exp $

you'll now write:

# /packages/acs-kernel/00-proc-procs.tcl
ad_library {

    Routines for defining procedures and libraries of procedures (<code>-procs.tcl</code>
    files).

    @creation-date 7 Jun 2000
    @author Jon Salz (jsalz@mit.edu)
    @cvs-id $Id: tcl-doc.html,v 1.43 2006/07/17 05:38:32 torbenb Exp $

}

Note that format is derived heavily from Javadoc: a general description of the script's functionality, followed optionally by a series of named attributes tagged by at symbols (@). HTML formatting is allowed. You are encouraged to provide:

  • An @author tag for each author. Specify the author's name, followed his or her email address in parentheses.

  • A @creation-date tag indicating when the script was first created.

  • A @cvs-id tag containing the page's CVS identification string. Just use $Id: tcl-documentation.html,v 1.2 2000/09/19 07:22:35 ron Exp $ when creating the file, and CVS will substitute an appropriate string when you check the file in.

Database Access API

Created by Gustaf Neumann, last modified by Gustaf Neumann 17 Feb 2008, at 07:08 AM

By Jon Salz. Revised and expanded by Roberto Mello (rmello at fslc dot usu dot edu), July 2002.

OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff.
  • Tcl procedures: /packages/acs-kernel/10-database-procs.tcl

  • Tcl initialization: /packages/acs-kernel/database-init.tcl

One of OpenACS's great strengths is that code written for it is very close to the database. It is very easy to interact with the database from anywhere within OpenACS. Our goal is to develop a coherent API for database access which makes this even easier.

There were four significant problems with the way OpenACS previously used the database (i.e., directly through the ns_db interface):

  1. Handle management. We required code to pass database handles around, and for routines which needed to perform database access but didn't receive a database handle as input, it was difficult to know from which of the three "magic pools" (main, subquery, and log) to allocate a new handle.

  2. Nested transactions. In our Oracle driver, begin transaction really means "turn auto-commit mode off" and end transaction means "commit the current transaction and turn auto-commit mode on." Thus if transactional code needed to call a routine which needed to operate transactionally, the semantics were non-obvious. Consider:

    proc foo { db args } {
        db_transaction {
          ...
        }
    }
    
    db_transaction {
    db_dml unused "insert into greeble(bork) values(33)"
    foo $db
    db_dml unused "insert into greeble(bork) values(50)"
    }
    
    

    This would insert greeble #33 and do all the stuff in foo transactionally, but the end transaction in foo would actually cause a commit, and greeble #50 would later be inserted in auto-commit mode. This could cause subtle bugs: e.g., in the case that the insert for greeble #50 failed, part of the "transaction" would have already have been committed!. This is not a good thing.

  3. Unorthodox use of variables. The standard mechanism for mapping column values into variables involved the use of the set_variables_after_query routine, which relies on an uplevel variable named selection (likewise for set_variables_after_subquery and subselection).

  4. Hard-coded reliance on Oracle. It's difficult to write code supporting various different databases (dynamically using the appropriate dialect based on the type of database being used, e.g., using DECODE on Oracle and CASE ... WHEN on Postgres).

The Database Access API addresses the first three problems by:

  1. making use of database handles transparent

  2. wrapping common database operations (including transaction management) in Tcl control structures (this is, after all, what Tcl is good at!)

It lays the groundwork for addressing the fourth problem by assigning each SQL statement a logical name. In a future version of the OpenACS Core, this API will translate logical statement names into actual SQL, based on the type of database in use. (To smooth the learning curve, we provide a facility for writing SQL inline for a "default SQL dialect", which we assume to be Oracle for now.)

To be clear, SQL abstraction is not fully implemented in OpenACS 3.3.1. The statement names supplied to each call are not used by the API at all. The API's design for SQL abstraction is in fact incomplete; unresolved issues include:

  • how to add WHERE clause criteria dynamically

  • how to build a dynamic ORDER BY clause (Ben Adida has a proposed solution for this)

  • how to define a statement's formal interface (i.e., what bind variables it expects, what columns its SELECT clause must contain if it's a query) without actually implementing the statement in a specific SQL dialect

So why is the incremental change of adding statement naming to the API worth the effort? It is worth the effort because we know that giving each SQL statement a logical name will be required by the complete SQL abstraction design. Therefore, we know that the effort will not be wasted, and taking advantage of the new support for bind variables will already require code that uses 3.3.0 version of the API to be updated.

set_variables_after_query is gone! (Well, it's still there, but you'll never need to use it.) The new API routines set local variables automatically. For instance:

db_1row select_names "select first_names, last_name from users where user_id = [ad_get_user_id]"
doc_body_append "Hello, $first_names $last_name!"

Like ns_db 1row, this will bomb if the query doesn't return any rows (no such user exists). If this isn't what you want, you can write:

if { [db_0or1row select_names "select first_names, last_name from users where user_id = [ad_get_user_id]"] } {
    doc_body_append "Hello, $first_names $last_name!"
} else {
    # Executed if the query returns no rows.
    doc_body_append "There's no such user!"
}

Selecting a bunch of rows is a lot prettier now:

db_foreach select_names "select first_names, last_name from users" {
     doc_body_append "Say hi to $first_names $last_name for me!<br>"
}

That's right, db_foreach is now like ns_db select plus a while loop plus set_variables_after_query plus an if statement (containing code to be executed if no rows are returned).

db_foreach select_names "select first_names, last_name from users where last_name like 'S%'" {
     doc_body_append "Say hi to $first_names $last_name for me!<br>"
} if_no_rows {
     doc_body_append "There aren't any users with last names beginnings with S!"
}

The new API keeps track of which handles are in use, and automatically allocates new handles when they are necessary (e.g., to perform subqueries while a select is active). For example:

doc_body_append "<ul>"
db_foreach select_names "select first_names, last_name, user_id from users" {
    # Automatically allocated a database handle from the main pool.
    doc_body_append "<li>User $first_names $last_name\n<ul>"

    db_foreach select_groups "select group_id from user_group_map where user_id = $user_id" {
        # There's a selection in progress, so we allocated a database handle
        # from the subquery pool for this selection.
        doc_body_append "<li>Member of group #$group_id.\n"
    } if_no_rows {
        # Not a member of any groups.
        doc_body_append "<li>Not a member of any group.\n"
    }
}
doc_body_append "</ul>"
db_release_unused_handles

A new handle isn't actually allocated and released for every selection, of course - as a performance optimization, the API keeps old handles around until db_release_unused_handles is invoked (or the script terminates).

Note that there is no analogue to ns_db gethandle - the handle is always automatically allocated the first time it's needed.

Introduction

Most SQL statements require that the code invoking the statement pass along data associated with that statement, usually obtained from the user. For instance, in order to delete a WimpyPoint presentation, a Tcl script might use the SQL statement

delete from wp_presentations where presentation_id = some_presentation_id

where some_presentation_id is a number which is a valid presentation ID of the presentation I want to delete. It's easy to write code handling situations like this since SQL statements can include bind variables, which represent placeholders for actual data. A bind variable is specified as a colon followed by an identifier, so the statement above can be coded as:

db_dml presentation_delete {
    delete from wp_presentations where presentation_id = :some_presentation_id
}

When this SQL statement is invoked, the value for the bind variable :some_presentation_id is pulled from the Tcl variable $some_presentation_id (in the caller's environment). Note that bind variables are not limited to one per statement; you can use an arbitrary number, and each will pull from the correspondingly named Tcl variable. (Alternatively, you can also specify an list or ns_set providing bind variables' values; see Usage.)

The value of a bind variable is taken literally by the database driver, so there is never any need to put single-quotes around the value for a bind variable, or to use db_quote to escape single-quotes contained in the value. The following works fine, despite the apostrophe:

set exclamation "That's all, folks!"
db_dml exclamation_insert { insert into exclamations(exclamation) values(:exclamation) }

Note that you can use a bind variable in a SQL statement only where you could use a literal (a number or single-quoted string). Bind variables cannot be placeholders for things like SQL keywords, table names, or column names, so the following will not work, even if $table_name is set properly:

select * from :table_name

Why Bind Variables Are Useful

Why bother with bind variables at all - why not just write the Tcl statement above like this:

db_dml presentation_delete "
    delete from wp_presentations where presentation_id = $some_presentation_id
"

(Note the use of double-quotes to allow the variable reference to $some_presentation_id to be interpolated in.) This will work, but consider the case where some devious user causes some_presentation_id to be set to something like '3 or 1 = 1', which would result in the following statement being executed:

delete from wp_presentations where presentation_id = 3 or 1 = 1

This deletes every presentation in the database! Using bind variables eliminates this gaping security hole: since bind variable values are taken literally. Oracle will attempt to delete presentations whose presentation ID is literally '3 or 1 = 1' (i.e., no presentations, since '3 or 1 = 1' can't possibly be a valid integer primary key for wp_presentations. In general, since Oracle always considers the values of bind variables to be literals, it becomes more difficult for users to perform URL surgery to trick scripts into running dangerous queries and DML.

Usage

Every db_* command accepting a SQL command as an argument supports bind variables. You can either

  • specify the -bind switch to provide a set with bind variable values, or

  • specify the -bind switch to explicitly provide a list of bind variable names and values, or

  • not specify a bind variable list at all, in which case Tcl variables are used as bind variables.

The default behavior (i.e., if the -bind switch is omitted) is that these procedures expect to find local variables that correspond in name to the referenced bind variables, e.g.:

set user_id 123456
set role "administrator"

db_foreach user_group_memberships_by_role {
    select g.group_id, g.group_name
    from user_groups g, user_group_map map
    where g.group_id = map.user_id
    and map.user_id = :user_id
    and map.role = :role
} {
    # do something for each group of which user 123456 is in the role
    # of "administrator"
}

The value of the local Tcl variable user_id (123456) is bound to the user_id bind variable.

The -bind switch can takes the name of an ns_set containing keys for each bind variable named in the query, e.g.:

set bind_vars [ns_set create]
ns_set put $bind_vars user_id 123456
ns_set put $bind_vars role "administrator"

db_foreach user_group_memberships_by_role {
    select g.group_id, g.group_name
    from user_groups g, user_group_map map
    where g.group_id = map.user_id
    and map.user_id = :user_id
    and map.role = :role
} -bind $bind_vars {
    # do something for each group in which user 123456 has the role
    # of "administrator"
}

Alternatively, as an argument to -bind you can specify a list of alternating name/value pairs for bind variables:

db_foreach user_group_memberships_by_role {
    select g.group_id, g.group_name
    from user_groups g, user_group_map map
    where g.group_id = map.user_id
    and map.user_id = :user_id
    and map.role = :role
} -bind [list user_id 123456 role "administrator"] {
    # do something for each group in which user 123456 has the role
    # of "administrator"
}

Nulls and Bind Variables

When processing a DML statement, Oracle coerces empty strings into null. (This coercion does not occur in the WHERE clause of a query, i.e. col = '' and col is null are not equivalent.)

As a result, when using bind variables, the only way to make Oracle set a column value to null is to set the corresponding bind variable to the empty string, since a bind variable whose value is the string "null" will be interpreted as the literal string "null".

These Oracle quirks complicate the process of writing clear and abstract DML difficult. Here is an example that illustrates why:

#
# Given the table:
#
#   create table foo (
#           bar        integer,
#           baz        varchar(10)
#   );
#

set bar ""
set baz ""

db_dml foo_create "insert into foo(bar, baz) values(:bar, :baz)"
#
# the values of the "bar" and "baz" columns in the new row are both
# null, because Oracle has coerced the empty string (even for the
# numeric column "bar") into null in both cases

Since databases other than Oracle do not coerce empty strings into null, this code has different semantics depending on the underlying database (i.e., the row that gets inserted may not have null as its column values), which defeats the purpose of SQL abstraction.

Therefore, the Database Access API provides a database-independent way to represent null (instead of the Oracle-specific idiom of the empty string): db_null.

Use it instead of the empty string whenever you want to set a column value explicitly to null, e.g.:

set bar [db_null]
set baz [db_null]

db_dml foo_create "insert into foo(bar, baz) values(:bar, :baz)"
#
# sets the values for both the "bar" and "baz" columns to null

We now require that each SQL statement be assigned a logical name for the statement that is unique to the procedure or page in which it is defined. This is so that (eventually) we can implement logically named statements with alternative SQL for non-Oracle databases (e.g., Postgres). More on this later.

Normally, db_foreach, db_0or1row, and db_1row places the results of queries in Tcl variables, so you can say:

db_foreach users_select "select first_names, last_name from users" {
    doc_body_append "<li>$first_names $last_name\n"
}

However, sometimes this is not sufficient: you may need to examine the rows returned, to dynamically determine the set of columns returned by the query, or to avoid collisions with existing variables. You can use the -column_array and -column_set switches to db_foreach, db_0or1row, and db_1row to instruct the database routines to place the results in a Tcl array or ns_set, respectively, where the keys are the column names and the values are the column values. For example:

db_foreach users_select "select first_names, last_name from users" -column_set columns {
    # Now $columns is an ns_set.
    doc_body_append "<li>"
    for { set i 0 } { $i < [ns_set size $columns] } { incr i } {
        doc_body_append "[ns_set key $columns $i] is [ns_set value $columns $i]. \n"
    }
}

will write something like:

  • first_names is Jon. last_name is Salz.

  • first_names is Lars. last_name is Pind.

  • first_names is Michael. last_name is Yoon.

Note that you never have to use ns_db anymore (including ns_db gethandle)! Just start doing stuff, and (if you want) call db_release_unused_handles when you're done as a hint to release the database handle.

db_null
db_null

Returns a value which can be used in a bind variable to represent the SQL value null. See Nulls and Bind Variables above.

db_foreach
db_foreachstatement-name sql [ -bind bind_set_id | -bind bind_value_list ]  [ -column_array array_name | -column_set set_name ]  code_block [ if_no_rows if_no_rows_block ]

Performs the SQL query sql, executing code_block once for each row with variables set to column values (or a set or array populated if -column_array or column_set is specified). If the query returns no rows, executes if_no_rows_block (if provided).

Example:

db_foreach select_foo "select foo, bar from greeble" {
    doc_body_append "<li>foo=$foo; bar=$bar\n"
} if_no_rows {
    doc_body_append "<li>There are no greebles in the database.\n"
}

The code block may contain break statements (which terminate the loop and flush the database handle) and continue statements (which continue to the next row of the loop).

db_1row
db_1rowstatement-namesql [ -bind bind_set_id | -bind bind_value_list ]  [ -column_array array_name | -column_set set_name ]

Performs the SQL query sql, setting variables to column values. Raises an error if the query does not return exactly 1 row.

Example:

db_1row select_foo "select foo, bar from greeble where greeble_id = $greeble_id"
# Bombs if there's no such greeble!
# Now $foo and $bar are set.

db_0or1row
db_0or1rowstatement-namesql [ -bind bind_set_id | -bind bind_value_list ]  [ -column_array array_name | -column_set set_name ]

Performs the SQL query sql. If a row is returned, sets variables to column values and returns 1. If no rows are returned, returns 0. If more than one row is returned, throws an error.

db_string
db_stringstatement-namesql [ -default default ] [ -bind bind_set_id | -bind bind_value_list ]

Returns the first column of the result of SQL query sql. If sql doesn't return a row, returns default (or throws an error if default is unspecified). Analogous to database_to_tcl_string and database_to_tcl_string_or_null.

db_nextval
db_nextvalsequence-name

Returns the next value for the sequence sequence-name (using a SQL statement like SELECTsequence-name.nextval FROM DUAL). If sequence pooling is enabled for the sequence, transparently uses a value from the pool if available to save a round-trip to the database.

db_list
db_liststatement-namesql [ -bind bind_set_id | -bind bind_value_list ]

Returns a Tcl list of the values in the first column of the result of SQL query sql. If sql doesn't return any rows, returns an empty list. Analogous to database_to_tcl_list.

db_list_of_lists
db_list_of_listsstatement-namesql [ -bind bind_set_id | -bind bind_value_list ]

Returns a Tcl list, each element of which is a list of all column values in a row of the result of SQL query sql. If sql doesn't return any rows, returns an empty list. (Analogous to database_to_tcl_list_list.)

db_list_of_ns_sets
db_list_of_ns_setsstatement-namesql [ -bind bind_set_id | -bind bind_value_list ]

Returns a list of ns_sets with the values of each column of each row returned by the sql query specified.

db_dml
db_dmlstatement-namesql  [ -bind bind_set_id | -bind bind_value_list ]  [ -blobs blob_list | -clobs clob_list |
      -blob_files blob_file_list | -clob_files clob_file_list ]

Performs the DML or DDL statement sql.

If a length-n list of blobs or clobs is provided, then the SQL should return n blobs or clobs into the bind variables :1, :2, ... :n. blobs or clobs, if specified, should be a list of individual BLOBs or CLOBs to insert; blob_files or clob_files, if specified, should be a list of paths to files containing the data to insert. Only one of -blobs, -clobs, -blob_files, and -clob_files may be provided.

Example:

db_dml insert_photos "
        insert photos(photo_id, image, thumbnail_image)
        values(photo_id_seq.nextval, empty_blob(), empty_blob())
        returning image, thumbnail_image into :1, :2
    "  -blob_files [list "/var/tmp/the_photo" "/var/tmp/the_thumbnail"]

This inserts a new row into the photos table, with the contents of the files /var/tmp/the_photo and /var/tmp/the_thumbnail in the image and thumbnail columns, respectively.

db_write_clob, db_write_blob, db_blob_get_file
db_write_clobstatement-namesql [ -bind bind_set_id | -bind bind_value_list ]

db_write_blobstatement-namesql [ -bind bind_set_id | -bind bind_value_list ]

db_blob_get_filestatement-namesql [ -bind bind_set_id | -bind bind_value_list ]

Analagous to ns_ora write_clob/write_blob/blob_get_file.

db_release_unused_handles
db_release_unused_handles

Releases any allocated, unused database handles.

db_transaction
db_transactioncode_block [ on_error { code_block } ]

Executes code_block transactionally. Nested transactions are supported (end transaction is transparently ns_db dml'ed when the outermost transaction completes). The db_abort_transaction command can be used to abort all levels of transactions. It is possible to specify an optional on_error code block that will be executed if some code in code_block throws an exception. The variable errmsg will be bound in that scope. If there is no on_error code, any errors will be propagated.

Example:

proc replace_the_foo { col } {
    db_transaction {
        db_dml "delete from foo"
        db_dml "insert into foo(col) values($col)"
    }
}

proc print_the_foo {} {
    doc_body_append "foo is [db_string "select col from foo"]<br>\n"
}

replace_the_foo 8
print_the_foo ; # Writes out "foo is 8"

db_transaction {
    replace_the_foo 14
    print_the_foo ; # Writes out "foo is 14"
    db_dml "insert into some_other_table(col) values(999)"
    ...
    db_abort_transaction
} on_error {
    doc_body_append "Error in transaction: $errmsg"
}


print_the_foo ; # Writes out "foo is 8"

db_abort_transaction
db_abort_transaction

Aborts all levels of a transaction. That is if this is called within several nested transactions, all of them are terminated. Use this insetead of db_dml "abort" "abort transaction".

db_multirow
db_multirow [ -local ] [ -append ] [ -extend column_list ]  var-namestatement-namesql  [ -bind bind_set_id | -bind bind_value_list ]  code_block [ if_no_rows if_no_rows_block ]

Performs the SQL query sql, saving results in variables of the form var_name:1, var_name:2, etc, setting var_name:rowcount to the total number of rows, and setting var_name:columns to a list of column names.

Each row also has a column, rownum, automatically added and set to the row number, starting with 1. Note that this will override any column in the SQL statement named 'rownum', also if you're using the Oracle rownum pseudo-column.

If the -local is passed, the variables defined by db_multirow will be set locally (useful if you're compiling dynamic templates in a function or similar situations).

You may supply a code block, which will be executed for each row in the loop. This is very useful if you need to make computations that are better done in Tcl than in SQL, for example using ns_urlencode or ad_quotehtml, etc. When the Tcl code is executed, all the columns from the SQL query will be set as local variables in that code. Any changes made to these local variables will be copied back into the multirow.

You may also add additional, computed columns to the multirow, using the -extend { col_1col_2 ... } switch. This is useful for things like constructing a URL for the object retrieved by the query.

If you're constructing your multirow through multiple queries with the same set of columns, but with different rows, you can use the -append switch. This causes the rows returned by this query to be appended to the rows already in the multirow, instead of starting a clean multirow, as is the normal behavior. The columns must match the columns in the original multirow, or an error will be thrown.

Your code block may call continue in order to skip a row and not include it in the multirow. Or you can call break to skip this row and quit looping.

Notice the nonstandard numbering (everything else in Tcl starts at 0); the reason is that the graphics designer, a non programmer, may wish to work with row numbers.

Example:

db_multirow -extend { user_url } users users_query {
    select user_id first_names, last_name, email from cc_users
} {
    set user_url [acs_community_member_url -user_id $user_id]
}
    
db_resultrows
db_resultrows

Returns the number of rows affected or returned by the previous statement.

db_with_handle
db_with_handlevarcode_block

Places a database handle into the variable var and executes code_block. This is useful when you don't want to have to use the new API (db_foreach, db_1row, etc.), but need to use database handles explicitly.

Example:

proc lookup_the_foo { foo } {
    db_with_handle db {
        return [db_string unused "select ..."]
    }
}

db_with_handle db {
    # Now there's a database handle in $db.
    set selection [ns_db select $db "select foo from bar"]
    while { [ns_db getrow $db $selection] } {
        set_variables_after_query

        lookup_the_foo $foo
    }
}

db_name
db_name

Returns the name of the database, as returned by the driver.

db_type
db_type

Returns the RDBMS type (i.e. oracle, postgresql) this OpenACS installation is using. The nsv ad_database_type is set up during the bootstrap process.

db_compatible_rdbms_p
db_compatible_rdbms_p db_type
		

Returns 1 if the given db_type is compatible with the current RDBMS.

db_package_supports_rdbms_p
db_package_supports_rdbms_p db_type_list
		

Returns 1 if db_type_list contains the current RDMBS type. A package intended to run with a given RDBMS must note this in it's package info file regardless of whether or not it actually uses the database.

db_legacy_package_p
db_legacy_package_p db_type_list
		

Returns 1 if the package is a legacy package. We can only tell for certain if it explicitly supports Oracle 8.1.6 rather than the OpenACS more general oracle.

db_version
db_version

Returns the RDBMS version (i.e. 8.1.6 is a recent Oracle version; 7.1 a recent PostgreSQL version.

db_current_rdbms
db_current_rdbms

Returns the current rdbms type and version.

db_known_database_types
db_known_database_types

Returns a list of three-element lists describing the database engines known to OpenACS. Each sublist contains the internal database name (used in file paths, etc), the driver name, and a "pretty name" to be used in selection forms displayed to the user.

The nsv containing the list is initialized by the bootstrap script and should never be referenced directly by user code. Returns the current rdbms type and version.

Next Page