This section focuses on the RF API from an application developer's point of view. As mentioned in RF Concepts, applications are built using the
current API, which consists of some portions of the com.sapportals.wcm.repository.* packages and of some portions of the
com.sapportals.wcm.util.* packages.
For simplified reading, these packages have been abbreviated as follows:
com.sapportals.wcm.repository is omitted
com.sapportals.wcm.repository.<subpackage> as
<subpackage>
com.sapportals.wcm.util as …util
Table of Contents
IResource represents an RF object (resource).
ICollection represents a collection and extends IResource.
IResource in turn extends ITypeCast, which represents a semantic object (see RF Concepts).
The class …util.uri.RID represents an RID. …util.uri.RID and offers the
static factory method getRID(), for creating an RID from a String.
IResourceContext represents a resource context, where ResourceContext is the RF's corresponding implementation. To create a
ResourceContext, at least the user on whose behalf the resource context is being created must be supplied in user information and the
portal's user management.
The main entry point to the RF is the resource factory, represented by IResourceFactory. An instance of the
IResourceFactory is retrieved by calling the ResourceFactory's static method getInstance().
The following sample code illustrates how to retrieve the IResourceFactory, create a resource context, and retrieve the resource for the RID
/etc:
com.sapportals.portal.security.usermanagement.IUser user = …
IResourceContext resourceContext = new ResourceContext(user);
RID rid = RID.getRID("/etc");
try {
IResource resource = ResourceFactory.getInstance()
.getResource(rid, resourceContext);
if( resource != null ) {
// resource found
System.out.println("resource " + resource.getRID() + " found");
} else {
// resource not found
System.out.println("resource " + resource.getRID() + " does not exist");
}
}
catch( ResourceException e ) {
// problem while retrieving the resource
System.out.println(
"exception while trying to get resource " + e.getRID()
+ ": " + e.getMessage()
);
}
A resource's RID and its resource context can be retrieved with getRID() and getContext().
A collection is retrieved in the same way as a resource and is then simply casted to ICollection:
if( resource.isCollection() ) {
ICollection collection = (ICollection)resource;
IResourceList children = collection.getChildren();
}
The children of a collection are returned by the ICollection's getChildren() method.
The paths and names, as used for repositories resources, might be restricted in length or by the set of available characters that can be used.
A client can retrieve this information by calling the manager.IRepositoryManager's getNameInfo() method:
ICollection root = … NameInfo nameInfo = root.getRepository().getNameInfo()
A NameInfo offers the following methods:
getMaxPathLength() returns the maximum length allowed for an RID's path part.
getMaxNameLength() returns the maximum length allowed for an RID's name part (and thus for the path segments, too).
getReservedResourceNameChars() returns the array of forbidden characters. These characters are not allowed for resource names (where resources are not collections).
getReservedCollectionNameChars() returns an array of forbidden characters that are not allowed for collection names (and thus not for the path
segments).
checkName() throws an InvalidNameException if the given resource name contains characters from
getReservedResourceNameChars().
Links are not represented by a dedicated class - the relevant operations are assigned to the IResource interface. Internal and external
link types are identified by appropriate enum.LinkType values, as returned by the IResource's getLinkType()
method:
if( LinkType.INTERNAL.equals(resource.getLinkType()) ) {
// resource is an internal link to another resource
IResource target = resource.getTargetResource();
if( target == null ) {
// link is broken, because target does not exist anymore
URL targetURL = resource.getTargetURL();
}
} else if( LinkType.EXTERNAL.equals(resource.getLinkType()) ) {
// resource is an external link to an URL
URL target = resource.getTargetURL();
} else { // if( LinkType.NONE.equals(resource.getLinkType()) )
// resource is not a link
}
The method getTargetResource()returns the target resource of an internal link - if it is not broken and if the target resource exists. The
method getTargetURL() returns the URL for an external link's target or the RID of an internal link's target (which might be relative, as it is
returned as passed when setTargetResource() is called or the link was created, see below).
As with EP 6.0 SP 1, flexible links or dynamic links are now supported within the RF. In any case, the RF offers no methods that allow a client to control whether an internal link is to be created as a static or dynamic link - this decision is up to the repository.
A client is only able to determine whether an internal link is a flexible (dynamic) link or just an internal (static) link: To do this, the
IResource has to be "casted" to an IInternalLinkResource semantic object.
if( LinkType.INTERNAL.equals(resource.getLinkType()) ) {
if( resource.isA(IInternalLinkResource.class) ) {
IInternalLinkResource internalLink = (IInternalLinkResource)resource
.as(IInternalLinkResource.class);
if( internalLink.isDynamic() ) {
// it's a flexible link (dynamic internal link), which never becomes broken
} else {
// it's a static internal link, which might be broken
}
}
}
Since only collections may contain resources, only ICollection offers methods for creating resources.
To create a resource, the client must provide a name for the resource in the collection. The collection where the
create...()-operation was requested becomes the parent collection of the newly created resource.
ICollection offers the following create operations:
createResource() for a plain resource with content and properties
createCollection() for a collection with properties
createLink() for a link with properties, link type, and target
IResourceContext context = new ResourceContext(user);
ICollection parent = (ICollection) ResourceFactory.getInstance()
.getResource("/documents", context);
IResource resource = parent.createResource("file", null, null);
ICollection collection = parent.createCollection("folder", null);
ILink link = parent.createLink("link", "/documents/file", LinkType.INTERNAL, null);
The example above retrieves the /documents folder and creates the empty resource (without properties or content) /documents/file,
the collection /documents/folder without properties, and the internal link /documents/link (which points to
/documents/file) in it.
The other namespace operations for copying, deleting, moving and renaming are available from the IResource interface:
copy() copies the resource and expects the destination RID and an optional ICopyParameter. This parameter is a descriptor (see
RF Concepts) that might supply additional parameters (for example, whether children should be copied recursively, too).
delete() deletes the resource, collection or link itself. When a collection is deleted, all its children are deleted, too.
move() moves the resource to another parent collection and expects the same parameters as the copy() method.
rename()changes the names of a resource and expects the resource's new name as a parameter.
Using the resource from the example above:
IResource copied = resource.copy("/documents/copy", null);
IResource moved = copied.move("/documents/folder", null);
IResource renamed = moved.rename("renamed");
renamed.delete();
This example creates a copy of /documents/file as /documents/copy, then moves it to the folder /documents/folder
as /documents/folder/copy and then renames it as /documents/folder/renamed.
When delete() is called, all the IResources are invalidated, since all of them reference the same resource. In other words,
calling copied.delete() or moved.delete() after renamed has been deleted, causes a ResourceException
to be thrown.
Content is represented by …util.content.IContent. The RF provides the class Content as a default
implementation.
For retrieving the content of a resource, getContent() is used.
IContent oldContent = resource.getContent();
The content is set either during the creation of a resource (see create) or by using updateContent():
String dataString = new String("content");
ByteArrayInputStream dataStream = new ByteArrayInputStream(dataString.getBytes());
IContent newContent = new Content(
dataStream,
"text/plain",
dataStream.available()
);
resource.updateContent(newContent);
The example above sets the resource's content to the text "content" as plain text.
Changing the content of a resource also updates the values of the corresponding live properties for content length, content type, and encoding, as given by
the IContent object.
As another side effect, the values of the other content-related live properties (entity tag, last modified, last modified by) are also updated accordingly.
A property's read-only operations are assigned to IProperty. The corresponding implementation is Property. The write operations
are assigned to IMutableProperty, with MutableProperty as the corresponding implementation.
The name of a property is modeled as IPropertyName, with PropertyName as the implementing class. IPropertyName is
derived from …util.name.IName, which holds the tuple of (namespace, name).
The property's attributes are modeled as java.util.Properties.
Each property has an enum.PropertyType, which defines the type of the property's value:
PropertyType.BOOLEAN for boolean values
PropertyType.DATE for Date values
PropertyType.INT for int values
PropertyType.LONG for long values
PropertyType.STRING for String values
PropertyType.XML for XML values
Please note that a property with a specific name is either set to an existing value or it does not exist. It is therefore not possible to store a
null value as a property's value. An undefined property value should be represented by a non-existing property.
The following example shows how a property is retrieved:
String namespace = "http://com.sap.netweaver.bc.rf.sample/xmlns/sample";
String name = "property";
IPropertyName propertyName = new PropertyName(namespace, name);
IProperty property = resource.getProperty(propertyName);
if( property != null ) {
// property exists
String value = property.getValueAsString();
} else {
// property is not set for this resource
}
The following example shows how to use the enum.PropertyType for checking the type of the property's value:
if( property.isMultivalued() ) {
// property is multi valued
List values = property.getValues();
if( values.size() > 0 ) {
// property is multi valued and a value element exists
if( PropertyType.BOOLEAN.equals(property.getType()) ) {
boolean firstValue = ((Boolean)list.iterator().next()).booleanValue();
} else if { PropertyType.DATE.equals(property.getType()) ) {
Date firstValue = (Date)list.iterator().next();
} else if { PropertyType.INT.equals(property.getType()) ) {
int firstValue = ((Integer)list.iterator().next()).intValue();
} else if { PropertyType.LONG.equals(property.getType()) ) {
long firstValue = ((Long)list.iterator().next()).longValue();
} else { // PropertyType.STRING and PropertyType.XML are mapped to String
String firstValue = (String)list.iterator().next();
}
} else {
// property is multi valued but has no value element
}
} else {
if( PropertyType.BOOLEAN.equals(property.getType()) ) {
boolean value = property.getBooleanValue();
} else if { PropertyType.DATE.equals(property.getType()) ) {
Date value = property.getDateValue();
} else if { PropertyType.INT.equals(property.getType()) ) {
int value = property.getIntValue();
} else if { PropertyType.LONG.equals(property.getType()) ) {
long value = property.getLongIntValue();
} else { // PropertyType.STRING and PropertyType.XML are mapped to String
String value = property.getValueAsString();
}
}
Setting a property is achieved as follows:
boolean value = true; IProperty property = new Property(propertyName, value); resource.setProperty(property);
This creates a property with a boolean type and value.
When changing the value of an already created or retrieved property, IMutableProperty is used:
IMutableProperty mutableProperty = property.getMutable(); mutableProperty.setBooleanValue(true); resource.setProperty(property);
This sets the value of the boolean property to true. If the new value causes a type mismatch with the previous value
(for example, assigning a string as a value for a property with integer type), a ResourceException is thrown.
Since creating and disposing of a large number of objects has an impact on the VM's garbage collector, Propertys and PropertyNames
should be reused where possible.
Therefore, the class PropertyName offers several methods for retrieving PropertyNames for the RF's live properties (see below for
examples of display names and resource types).
The IPropertySearchManager's isUnderstood()method can be used to check whether a given property name identifies a live property
(see RF Extensions; on IPropertySearchManager).
The display name property is named as returned by PropertyName.createDisplayname(). The IResource offers the method
getDisplayName() as a shortcut for getProperty(PropertyName.createDisplayname()) with two different signatures:
The first one, without any parameter, just returns the display name, or null if it is not set.
// retrieving the display name or null if not set
String displayname = resource.getDisplayName(); // or .getDisplayName(false);
if( displayname == null ) {
// retrieving the RID's name if display name not set
displayname = resource.getRID().name(); // use RID's name as fallback
}
The second signature takes a boolean parameter: If false, it returns the same result as without any parameter. If
true, it returns the display name as shown in the example above. Therefore the example above could be reduced to:
// retrieving the display name or RID's name if not set String displayname = resource.getDisplayName(true);
If an application wants to update the display name, the Property class offers the createDisplaynameProp() method for creating a
display name property from a given string.
// update the display name resource.setProperty(Property.createDisplaynameProp(displayname));
The resource type property is handled in the same manner: It is represented by the property name as returned by
PropertyName.createResourceType() and the IResource offers getResourceType() as a shortcut for
getProperty(PropertyName.createResourceType()).
// retrieving the resource's type
String resourcetype = resource.getResourceType();
if( resourcetype == null ) {
// not set
}
To build a resource type property for updating a resource's type, Property.createPropertyTypeProp() creates a resource type property from the
given String.
Note: In order to avoid the ambiguous naming of resource types (the values of the resource type property), this resource type's property value has to
adhere to the following namespace scheme: All resource types of the RF are prefixed by the value defined with
com.sapportals.wcm.IWcmConst.RESOURCE_TYPE_NAMESPACE.FRAMEWORK, while the resource types of applications are prefixed by the value defined with
com.sapportals.wcm.IWcmConst.RESOURCE_TYPE_NAMESPACE.APPLICATIONS and separated by an IWcmConst.NAMESPACE_SEPARATOR. If the
application "myApp" wants to invent the new resource type value "myResourceType", the proper coding for updating the resource type
might then look like the following:
// myApp's namespace
public final String myTypeNS = IWcmConst.RESOURCE_TYPE_NAMESPACE.APPLICATIONS
+ IWcmConst.NAMESPACE_SEPARATOR
+ "myApp";
// myApp's full resource type value
public final String myTypeName = myTypeNS
+ IWcmConst.NAMESPACE_SEPARATOR
+ "myResourceType";
// setting a resource's resource type property to myTypeName
resource.setProperty(Property.createResourceTypeProp(myTypeName));
Ordered collections are ICollections that return enum.OrderType.MANUAL for getOrderType().
An application might enable ordering for an ICollection by using setOrderType(), if it is supported (otherwise, a
NotSupportedException is thrown).
If an application wants to reorder the order of an ordered collection's children, the definition for reordering is modeled as IReorderList,
with ReorderList as the implementation.
IReorderList defines several "positioning commands" as IPositioning (positioning for implementation). Such a "positioning command"
defines the resource to which it is to be supplied and the new position to choose for the resource in question.
The positions are defined as IPosition, where Position is the implementation. There are four kinds of positions, where the
"position kind" is defined by enum.OrderPosition:
OrderPosition.FIRST: The first element
OrderPosition.LAST: The last element
OrderPosition.BEFORE: The predecessor of another resource
OrderPosition.AFTER: The successor of another resource
The third and fourth OrderPositions are relative position kinds, and therefore require a resource to which they can refer in
relation.
Consider the collection /c, with getOrderType(), returning OrderType.MANUAL and that contains the following children
in the specified order:
1st, 2nd, 3rd, 4th
The following examples demonstrate the several OrderPositions:
IReorderList reordering = new ReorderList();
reordering.add(new Positioning("4th", new Position(null, OrderPosition.FIRST)));
reordering.add(new Positioning("1st", new Position(null, OrderPosition.LAST)));
reordering.add(new Positioning("2nd", new Position("1st", OrderPosition.BEFORE)));
reordering.add(new Positioning("3rd", new Position("4th", OrderPosition.AFTER)));
orderedCollection.reorder(reordering);
In this example's first step, 4th is positioned as the first element.
4th, 1st, 2nd, 3rd
Then 1st is positioned as the last element.
4th, 2nd, 3rd, 1st
Next, 2nd is positioned relatively before 1st (which is now the last element), making it the third element.
4th, 3rd, 1st, 2nd
The last step positions 3rd relatively after 4th (which is now the first element), making it the second element (which it is
already).
4th, 3rd, 1st, 2nd
Property queries are built using an IQueryBuilder. A property query consists of one or more IQueryExpressions that can be combined
using Boolean operations (and(), or() and not()). An IQueryExpression represents one WHERE
clause, defining the matching values for a given property as specified by its IPropertyName.
There are two ways of retrieving an IQueryBuilder:
search.IGenericQueryFactory offers a generic IQueryBuilder that
is applicable to any repository but is not optimized for a specific repository.
This is especially useful if a query spans several repositories.
manager.IPropertySearchManager, which can be retrieved from a resource's manager.IRepositoryManager, offers an
IQueryBuilder that is specialized for the given repository.
Unlike a query expression built with an IGenericQueryFactory, the query expression built by a specific repository's query builder cannot be used
with another repository.
To select the resources for the given query, execute() has to be called, depending on where the SELECT is to be called:
IQueryExpression must be converted into a search.IGenericQuery. Then the
search.IGenericQuery's execute() has to be called.
manager.IPropertySearchManager's (the one that returned the IQueryBuilder)
execute() has to be called.
For example, the following WHERE clause is to be used:
( '{http://sapportals.com/xmlns/cm}contenttype'like '%.jpg'
or '{http://sapportals.com/xmlns/cm}contenttype' like '%.gif'
) and '{http://sapportals.com/xmlns/cm}createdby'= 'admin'
This WHERE clause's corresponding IQueryExpression could be constructed with the following code:
ICollection start = (ICollection)factory.getResource("/documents", context);
IGenericQueryFactory queryFactory = GenericQueryFactory.getInstance();
IQueryBuilder queryBldr = queryFactory.getQueryBuilder();
IQueryExpression queryExpr =
queryBldr.like(PropertyName.createContentType(), "%.jpg").or(
queryBldr.like(PropertyName.createContentType(), "%.gif").and(
queryBld.eq(PropertyName.createCreatedBy(), "admin")
)
);
IGenericQuery query = queryFactory.toGenericQuery(queryExpr);
IResourceList result = query.execute(
start, // the resource to start at
Integer.MAX_VALUE, // maximum depth
Integer.MAX_VALUE, // maximum result size
false // don't include versions
);
The example above shows how the generic query is used.
Using the IResource's search() method is equivalent to query.execute():
… = start.search(query, Integer.MAX_VALUE, Integer.MAX_VALUE, false)
The following example shows the same thing for a repository-specific query, which is usually faster (since the query expression is already set up for the repository) but can only be applied to this type of repository:
ICollection start = (ICollection)factory.getResource("/documents", context);
IRepositoryManager repositoryMgr = start.getRepositoryManager();
IPropertySearchManager searchMgr = repositoryMgr.getPropertySearchManager(start);
IQueryBuilder queryBldr = searchMgr.getQueryBuilder();
IQueryExpression queryExpr =
queryBldr.like(PropertyName.createContentType(), "%.jpg").or(
queryBldr.like(PropertyName.createContentType(), "%.gif").and(
queryBld.eq(PropertyName.createCreatedBy(), "admin")
)
);
IResourceList result = searchMgr.execute(
queryExpr, // the WHERE clause
start, // the resource to start at
Integer.MAX_VALUE, // maximum depth
Integer.MAX_VALUE, // maximum result size
false // don't include versions
);
Note: If a query builder is not able to construct a query expression for a given property and operation, the returned query expression might be
null. The examples above do not check for null being returned, but "real" code should do so.
In the current API, unique IDs are provided by a global service. See RF Concepts for details.
Security constraints that are defined for the operations on the RF's resource aspects are handled by the repository manager (see RF Concepts). This means
that all a client application usually has to concern itself with is the proper handling of AccessDeniedExceptions. Such exceptions are thrown if
an operation has been requested by a user for a resource and the user does not have the necessary permissions.
For example:
// retrieval requires no permission
IResource resource = ResourceFactory.getInstance()
.getResource(RID.getRID("/documents/myResource"), context);
if( resource != null ) {
// resource exists
try {
resource.delete(); // request an operation, e.g. delete()
}
catch( AccessDeniedException e ) {
// show a message to the user
System.out.println(
e.getPermissionName() + " not granted for " +
+ e.getUserID() + " on "
+ e.getRID().name()
);
}
}
Please note that the resource can be retrieved even if the user has no permission at all for the resource specified by the given RID. Permissions are not
checked unless an operation that requires a specific permission is requested. The only method that does not require a permission for an IResource
is getRID().
To prevent users from "seeing" the children of a resource, the ICollection's getChildren() method checks for the "list children"
permission (see RF Concepts; for details on checked permissions).
An extension or application can use the manager.ISecurityManager's isAllowed() method to check whether a specific permission is
granted for a user:
ISecurityManager securityManager = resource.getRepositoryManager()
.getSecurityManager(resource);
if( securityManager.isAllowed(resource, user, IPermission.PERMISSION_DELETE) {
// delete permission granted
} else {
// delete permission denied
}
If an extension or application wants to establish its own security constraints, it can use the service ACLs (see RF Concepts) to define its own permissions and to define application-specific security semantics.
Both the repository manager's manager.IAclSecurityManager and the service.serviceacl.IAclService offer the
getAclManager() method for retrieving the corresponding security.IResourceAclManager.
The security.IResourceAclManager handles the retrieval, creation, and removal of the security.IResourceAcl - either those for the
repository manager or those of the service's ACL. The manager.IAclSecurityManager and service.serviceacl.IAclService use it to
retrieve their ACLs and perform the isAllowed() check.
An IResourceAclManager needs to know about available permissions, represented by
…util.acl.IAclPermission. Permissions are identified by their IDs. Permissions used by the RF are defined by the
constants in manager.IPermission.
After a permission has been created by createPermission(), it has to be assigned to a ..util.acl.IObjectType
as a supported permission before it can be used. To do this, an application or extension uses addSupportedPermission(), passing the
relevant object type and permission as the method's arguments. The IObjectType.OBJECT_TYPE_NODE is used for node resources (collections) and
IObjectType.OBJECT_TYPE_LEAF is used for leaf resources (resources that are not collections).
An ACL, represented as security.IResourceAcl, is bound to an IResource and has at least one owner. It can contain zero, one, or
more instances of security.IResourceAclEntry. These specify the permissions for a specific principal. ACLs are created with the createAcl() method,
which takes the resource to which the ACL is to be assigned as a parameter. Therefore, the created ACL initially only gives full control (all supported
permissions are granted) to the owner, taken from the resource's context.
An IResourceAclEntry contains the principal and the permission that is granted (or revoked, see below) for a user. An instance of a
IResourceAclEntry is created by the IResourceAclManager's createAclEntry() method, which takes the principal and the
permission at parameters. A flag for positive or negative entries is also supported. This flag indicates whether the entry grants or revokes a specific
permission, as well as a sort-index that controls when to apply a "revoke" or "grant" (for example, if an ACL has two entries, E1 for revoking
permission P for user U, and E2 for granting permission P to group G. User U is
member of group G. If E1 now has a lower sort index than E2, Permission P is revoked for user
U, but it is granted by E2 if E1 has a higher sort index than E2).
Note: Users became very confused when negative ACLs where used. The KM's ACL editor therefore does not support negative ACL entries or sort index.
A resource can be locked by using the IResource's lock() method. If no parameters are supplied, the resource is locked for the
user specified in the resource's context. The lock created is an exclusive, shallow write lock with an infinite timeout.
To specify another scope, depth, or timeout, the application has to supply the values bundled in ILockProperties, where
LockProperties is the corresponding implementation.
Scope, type, and depth are modeled as enum.LockScope, enum.LockType and enum.LockDepth.
enum.LockType.READ is not used in the RF at the moment.
The methods isLocked() and isLockedByMe() are used to check whether a resource is locked.
The methods refreshLock() und unlock() require the ILockInfo that was returned by lock() to refresh or
release a lock.
The ILockInfo supplies a lock token (a String that uniquely identifies the lock among the locks of the resource). This can be used
to retrieve the ILockInfo required for unlock() by calling getLockByToken().
For example:
ILockProperties lockProperties = new LockProperties(
LockScope.SHARED, // shared
LockType.WRITE, // write
LockType.SHALLOW, // shallow
(60 * 60 * 1000) // timeout 1h
);
ILockInfo lockInfo = resource.lock(lockProperties);
String lockToken = lockInfo.getLockToken();
…
ILockInfo restoredLockInfo = resource.getLockByToken(lockToken);
resource.unlock(restoredLockInfo);
This locks the resource with a shared, shallow write lock, with a timeout of one hour (60 minutes * 60 seconds * 1000 milliseconds).
The lock token can then be stored and later restored. For unlocking, the lock info is retrieved using the restored lock token.
Each repository manager owns a manager.IResourceEventBroker that can be retrieved by a call to the manager.IRepositoryManager's
getEventBroker() method.
The event broker is the central object where both senders and receivers are registered. Senders are represented by
manager.IResourceEventSender and receivers by manager.IResourceEventReceiver.
All these interfaces are derived from the corresponding interfaces in …util.events.*, simply by adding the
resource-related methods.
A generic event is modeled by …util.events.IEvent. The type is just an integer constant (defined in
manager.ResourceEvent) that specifies the event's type. The event's isLike() method checks whether two events are similar with
respect to their type and Java class (disregarding the parameter). The isLike() method treats the ResourceEvent.ALL type a little bit
differently than the other types: ResourceEvent.ALL matches any other type, so isLike() returns true when compared
to any other type of the same event Java class.
The parameter object returned by getParameter() is just optional information that is associated with this event (for example, the old RID of a
resource when it was moved).
A resource event manager.IResourceEvent is derived from …util.events.IEvent It adds the resource to
which the event refers to the event.
An application must implement the manager.IResourceEventReceiver interface in order to receive events:
public class MyEventReceiver
implements IResourceEventReceiver {
private IResourceEvent DELETE_EVENT
= new ResourceEvent(ResourceEvent.DELETE, null);
private static IResourceEvent MOVE_EVENT
= new ResourceEvent(ResourceEvent.MOVE, null);
private static IResourceEvent RENAME_EVENT
= new ResourceEvent(ResourceEvent.RENAME, null);
public MyEventReceiver() {
}
public void register(IResource resource)
throws WcmException {
IResourceEventBroker broker = resource.getRepositoryManager().getEventBroker();
if( broker == null ) return;
broker.register(this, DELETE_EVENT, IEventBroker.PRIO_MIN, true);
broker.register(this, MOVE_EVENT, IEventBroker.PRIO_MIN, true);
broker.register(this, RENAME_EVENT, IEventBroker.PRIO_MIN, true);
}
public void unregister(IResource resource)
throws WcmException {
IResourceEventBroker broker = resource.getRepositoryManager().getEventBroker();
if( broker == null ) return;
broker.unregister(this, DELETE_EVENT, IEventBroker.PRIO_MIN);
broker.unregister(this, MOVE_EVENT, IEventBroker.PRIO_MIN);
broker.unregister(this, RENAME_EVENT, IEventBroker.PRIO_MIN);
}
public void received(IEvent event) {
try {
if( event instanceof IResourceEvent ) {
IResourceEvent resourceEvent = (IResourceEvent)event;
switch( resourceEvent.getType() ) {
case ResourceEvent.DELETE:
// resourceEvent.getResource().getRID() was deleted
break;
case ResourceEvent.RENAME:
// RID.getRID((String)resourceEvent.getParameter()) was renamed
// to resourceEvent.getResource()
break;
case ResourceEvent.MOVE:
// resourceEvent.getResource() was moved to
// (IResource)resourceEvent.getParameter() was renamed
break;
default:
// ignore other IResourceEvents
}
}
}
catch( Exception e ) {
}
}
}
When the MyEventReceiver class is registered to a repository's event broker, it receives the events it registered for:
IResource resource = factory.getResource("/documents", context);
MyEventReceiver aReceiver = new MyEventReceiver();
aReceiver.register(resource);
This causes the MyEventReceiver to register itself with the event brokers of the given resource's (/documents) repository manager
for DELETE, MOVE and RENAME events. The receiver registers itself with minimum priority as an asynchronous receiver
(indicated by the additional boolean true parameter on registration).
The priority determines the event receiver's position in the list of the event broker's receivers.
If an event receiver wants to use the correlation ID, it must use the ResourceEvent class that provides the methods
getCorrelationId() and getUniqueId():
public void received(IEvent event) {
…
if( event instanceof ResourceEvent ) {
ResourceEvent resourceEvent event = (ResourceEvent)event;
String correlationId = resourceEvent.getCorrelationId();
switch( resourceEvent.getType() ) {
case ResourceEvent.PRE_DELETE:
// resourceEvent.getResource().getRID() is about to be deleted
break;
case ResourceEvent.DELETE:
// resourceEvent.getResource().getRID() was deleted
break;
}
}
…
}
The correlation ID identifies a sequence of events that belong together - for now, only corresponding pre-and post-events, as shown in the example above, are associated with the same correlation ID.
Once an event receiver has registered with an event broker, it receives events until it deregisters.
If a receiver performs an operation on a resource when handling a received event, this might trigger other events. In the worst case scenario, this can lead
to unintended recursions. To prevent this, an event receiver can change its state to "suspended" by calling the event broker's suspend() method.
This temporarily disables events being sent to the suspended receiver until it changes its state back to "receiving" by calling resume(). Events
that are triggered while the receiver is suspended are simply dropped. If the dropping of events is not desired, another mode, similar to suspend, is
available: "hold" is invoked by calling the event broker's hold() method. While on "hold", triggered events are collected by the event broker,
allowing the receiver to handle them later. Depending on the number of events being triggered, this might cause a memory overflow if the receiver is to slow in
resetting the list of collected events.
The following picture summarizes the states of an event receiver:
In order to send events, an application has to:
IResourceEventSender interface
IResourceEventSender implementation with the appropriate event broker
Defining a new event type:
public class MyEvent
implements IEvent {
public static final int MYEVENT = 4711;
private IResource parameter;
public MyEvent(IResource resource) {
this.parameter = resource;
}
public IResource getResource() {
return this.parameter();
}
public String getDescription() {
return getDescription(java.util.Locale.getDefault());
}
public String getDescription(java.util.Locale locale) {
return "MyEvent";
}
public Object getType() {
return null;
}
public boolean isLike(IEvent template) {
// since only one type-code is used, it's enough to check the Java class
return ( template instanceof MyEvent );
}
}
Please note that it might not be a good idea to derive new resource events from IResourceEvent, since most event receivers only check for
instanceof IResourceEvent and then use the value returned from getType() to select the proper event type.
If a new event implements IResourceEvent and uses a type code that is already used by another IResourceEvent
implementation, the event receivers also try to handle the new event.
Implementing a separate event sender for the IResourceEventSender interface:
public class MyEventSender
implements IResourceEventSender {
private static final MyEvent myEventTemplate = new MyEvent(null);
private IEventList myEventList = new EventList();
private IResourceEventBroker broker = null;
public MyEventSender() {
this.myEventList.add(myEventTemplate);
}
public void register(IResource resource)
throws WcmException {
if( this.broker != null ) broker.unregister(this);
this.broker = resource.getRepositoryManager().getEventBroker();
if( this.broker == null ) return;
broker.register(this);
}
public void unregister(IResource resource)
throws WcmException {
if( this.broker == null ) return;
this.broker.unregister(this);
this.broker = null;
}
public IEventList getEvents() {
return this.myEventList;
}
public IEventList getEvents(IResource resource) {
return this.myEventList;
}
public void send(IResource resource) {
try {
if( this.broker == null ) return;
this.broker.send(new MyEvent(resource), this);
}
catch( Exception e ) {
// handle exception
}
}
}
An instance of the MyEventSender class is registered with a repository's event broker as follows:
IResource resource = ResourceFactory.getInstance()
.getResource("/documents", context);
MyEventSender mySender = new MyEventSender();
resource.getRepositoryManager().getEventBroker().register(mySender);
To send a MyEvent::
mySender.send(resource);
If a receiver wants to handle the new type of event, its received() method might look like the following:
public void received(IEvent event) {
try {
if( event instanceof MyEvent ) {
MyEvent myEvent = (MyEvent)event;
// myEvent.getResource() returns the resource of MyEvent
} else if( event instanceof IResourceEvent ) {
IResourceEvent resourceEvent = (IResourceEvent)event;
// handle IResourceEvents here
}
// other event implementations are ignored
}
catch( Exception e ) {
}
}
Type handling is a mechanism that allows the RF's unified resources to be "casted" onto semantic objects (see RF Concepts).
The IObjectFactoryRegistry holds a list of all known IObjectFactorys: Each IObjectFactory has to register itself with
the IObjectFactoryRegistry; an instance of the IObjectFactoryRegistry is provided by
ObjectFactoryRegistry.getInstance():
IObjectFactoryRegistry objectFactoryRegistry = ObjectFactoryRegistry
.getInstance();
objectFactoryRegistry.registerObjectFactory(new MyObjectFactory());
The object factories are responsible for "casting" IResources onto other objects:
A IObjectFactory offers isA() to check whether a given resource can be "casted" onto an object of a specific class or interface.
To "cast" a resource into an object of a specific class or interface, the method as() is used. The method listTypes() lists all the
available classes and interfaces to which the given resource can be "casted".
A RF application might either implement its own object factory, which has to implement the IObjectFactory interface, or use the RF's
DefaultObjectFactory instead:
The DefaultObjectFactory implements the IMutableObjectFactory, which allows new classes to be registered as possible objects to
which a resource can be "casted". The DefaultObjectFactory uses the resource's type as the selection criteria. The resource type is specified by
the value of the resource type property (Property.createResourceTypeProp(), as returned by the IResource's
getResourceType() method. When a cast is requested, the DefaultObjectFactory uses the resource type and the
class to which the resource should be "casted" to select the appropriate object to be created.
For example:
IObjectFactoryRegistry objectFactoryRegistry = ObjectFactoryRegistry
.getInstance();
// create the list of interfaces
Vector interfaces = new Vector();
interfaces.add(IMyInterface.class);
// register the class and its interfaces with the default object factory
objectFactoryRegistry.getDefaultObjectFactory()
.registerClass(
MyObject.TypeName,
MyObject.class,
interfaces
);
Where MyInterface and MyObject might look like the following:
public interface MyInterface {
}
public class MyObject
extends AbstractTypeCast
implements IMyInterface {
static public String TypeName = IWcmConst.RESOURCE_TYPE_NAMESPACE.APPLICATIONS
+ IWcmConst.NAMESPACE_SEPARATOR + "myApp"
+ IWcmConst.NAMESPACE_SEPARATOR + "myType";
public MyObject(IResource resource) {
super(resource);
}
}
The example above registers MyObject as a "castable" object for resources with the resource type
"http://sap.com/xmlns/cm/app/myApp/myType". All resources of this type can then be converted into MyObjects by using
resource.as(IMyInterface.class) or resource.isA(MyObject.class).
The DefaultObjectFactory requires the registered class to be derived from AbstractTypeCast, because it relies on the specified
constructor, which takes an IResource as parameter.
While the DefaultObjectFactory uses the resource type, it is not possible to register several classes for the same resource type and interface
at the same time. To use a selection criterion other than resource type and class/interface, you could implement your own IObjectFactory
implementation.
For example:
public class MyObjectFactory
implements IObjectFactory {
private boolean isMyObject(resource) {
return resource.getRID().extension().equalsIgnoreCase("MYOBJ");
}
public boolean isA(Class objectClass,
IResource resource)
throws ResourceException {
if( ( isMyObject(resource) ) // check source of cast
&& ( objectClass.isAssignableFrom(MyObject.class) ) // check destination
) {
return true;
}
return false;
}
public Object as(Class objectClass,
IResource resource)
throws ResourceException {
if( this.isA(objectClass, resource) ) {
return new MyObject(resource);
} else {
return null;
}
}
public Collection listTypes(IResource resource) {
Collection allClasses = new ArrayList();
if( isMyObject(resource) ) {
Class[] objectClasses = MyObject.class.getClasses();
for( int i = 0; i < objectClasses.length; i++ ) {
allClasses.add(objectClasses[i]);
}
}
return allClasses;
}
}
The example uses the extension of a resource's RID to check whether it is a "castable" object (those resources with extension .MYOBJ in
this case).
Before a resource can be checked out, version control has to be enabled for the resource. This is achieved using the enableVersioning() method.
The method isVersioned() returns true if versioning is enabled (that is, if the resource is a VCR).
IResource resource = …;
// turn on versioning, if it's not already enabled
try {
if( !resource.isVersioned() ) {
resource.enableVersioning(true);
}
}
catch( OperationNotSupported e ) {
System.out.println("versioning not supported for " + resource.getRID());
}
The checkOut() method is used to check out a resource, while undoCheckOut() cancels the check-out state and checkIn()
performs a check-in, thereby creating a new version (also called revision) of the resource.
For example:
IResource resource = …; // check-out the VCR resource.checkOut(); IContent oldContent = resource.getContent(); // usually the user or some client application modifies the content or properties IContent newContent = …; // update content and properties and check-in the VCR resource.checkIn(newContent, null);
Note: the current API's checkIn() method is a convenience method that does two things: Firstly, it updates the VCR's version controlled state
from the content and properties supplied and secondly, it changes the check-out state to "checked-in". Although update and check-in are atomic
operations, the IResource's checkIn() method is not atomic.
It is also possible to perform only the check-in by specifying null as content or properties.
For example:
// first, update the content
String dataString = new String("new version");
ByteArrayInputStream data = new ByteArrayInputStream(dataString.getBytes());
IContent newContent = new Content(data, "text/plain", data.available());
resource.updateContent(newContent);
// then, only the check-in
ICheckInInfo checkInInfo = resource.checkIn(null, null);
The check-in returns an ICheckInInfo object that provides the version's ID (version number) and the RID of the version's resource.
A version returns true on isRevision().
As versioning is only related to the version-controlled state of a resource, there are no concepts of "ownership". There is therefore no way of determining who checked out a VCR. If an application wants to prevent concurrent modification, it should use locking. When combined with versioning, the resource is then locked before check-out and unlocked after check-in.
The expected RID, as returned by checkOut(), specifies the RID assigned to the revision that is created when the VCR is checked in. A client
can use that RID in advance, for example to create a reference to that revision in the content, before it is checked in.
If a repository returned an expected RID, it checks for the expected RID on checkIn()as follows: If the client specifies the expected RID on
check in, and it does not match the RID of the revision to be created on check-in (for example, because another application checked in a new version and then
checked the resource out again), an ExpectedCheckInRIDException is thrown.
This is an example of how to use the expected RID if the repository supports checking:
CheckOutInfo checkOutInfo = resource.checkOut();
// edit the resource
if( checkOutInfo != null ) {
// application might use checkOutInfo.getExpectedRevisionRID() here
try {
ICheckInInfo checkInInfo = resource.checkIn(
newContent, null, // content+props
true, // ignore property failures
checkOutInfo
.getExpectedRevisionRID()
);
}
catch( ExpectedCheckInRIDException e ) {
// expected RID would not match the new version's RID
}
} else { // repository returned null checkout-info -> doesn't support expected RID
ICheckInInfo checkInInfo = resource.checkIn(
newContent, null, // content+props
true // ignore property failures
);
}
Please note that this feature is optional.
The method getVersionHistory() is used to retrieve the (linear) version history of a resource under basic version control. It returns the list
of versions, sorted by age and starting with the oldest version. The last entry is the current version.
This method throws a NotSupportedException if the revision is not versioned or if it is versioned but does not have a linear version
history.
While the basic versioning features are located in the IResource interface, the advanced aspects are defined in other interfaces. In order for these features to be used, an IResource has to be "casted" to these interfaces first, using the type handling mechanism.
Even for basic versioning, the interfaces IVersionControlledResource (representing a VCR) and IVersionResource (representing a
version or revision) are available, but they are rarely used except for the advanced versioning features.
An IResource can be "casted" to a IVersionControlledResource semantic object if isVersioned() returns true. It can
be "casted" to an IVersionResource semantic object if isRevision() returns true. For example:
if( resource.isA(IVersionControlledResource.class) ) {
// resource is a VCR
IVersionControlledResource vcr = (IVersionControlledResource)resource
.as(IVersionControlledResource.class);
}
From a client's point of view, working resources just differ from in-place checkout in so far as the resource they are applied to: An in-place checkout refers to the (checked in) version controlled resource (VCR), whereas the checkout of a working resource refers to a version (revision).
A working resource can also serve as a temporary resource on the server side that can hold the content and properties of the version to be checked in (with basic versioning's in-place check-out, the client has to store the content and properties on its own).
Working resources are represented by the IWorkingResource interface and created by a checkOut() call for an
IVersionResource (while basic versioning works on IVersionControlledResources).
For example:
IResource resource = … IVersionHistory versions = resource.getVersionHistory(); IResource currentVersion = versions.get(version.size()-1); // now currentVersion is the current version of resource version.checkOut();
The methods for creating a workspace are located in the IExtendedCollection interface. To retrieve such an "extended collection", a normal
collection (for example, the repository's root collection) has to be "casted" to an IExtendedCollection semantic object:
ICollection collection = …
if( resource.isA(IExtendedCollection.class) ) {
IExtendedCollection extendendCollection = (IExtendendCollection)collection
.as(IExtendedCollection.class);
}
To create a workspace, the method createWorkspace() is called, which returns an ICollection representing an
IWorkspaceResource:
ICollection collection = …
if( collection.isA(IExtendedCollection.class) ) {
IExtendedCollection extendendCollection = (IExtendendCollection)collection
.as(IExtendedCollection.class);
ICollection workspace = extendedCollection.createWorkspace("myWorkspace", null);
}
This collection can then be treated as a normal collection, using the createCollection() and createResource() methods for
non-versioned collections and resources as usual.
To create a VCR within the namespace of the workspace, the method createVersionControlledResource() is used.
Consider the example in RF Extensions, where R1v5 is a revision of R1, and assume that /repository/Ra is the parent
of R1. To place R1v5 as a checked out workspace resource in the workspace, the code might look like the following:
IResource R1 = …
// create the parent in the workspace
IExtendedCollection Wa = (IExtendedCollection)workspace.createCollection(
"Ra",
null
)
.as(IExtendedCollection.class);
// get the version the workspace controlled resource should refer to:
IVersionHistory R1versions = R1.getVersionHistory(); // first get the history
IResource R1v5 = R1versions.getRevision("v5"); // then the "v5" version of R1
// now create the VCR for R1v5
IResource R1v5VCR = Wa.createVersionControlledResource(R1v5.getRID(), null);
// and finally check it out
R1v5VCR.checkOut();
Since R1v5VCR in the example above is a workspace controlled resource (WCR), it could be "casted" to an
IWorkspaceControlledResource semantic object. This interface provides getWorkspaceRID() for retrieving the RID of the workspace to
which the WCR belongs.
To check in the checked out resources of a workspace, the workspace has to be "casted" to an IWorkspaceResource semantic object. This interface
offers getCheckOutSet() for retrieving the RIDs of all checked out resources in this workspace.
For example:
IWorkspaceResource myWorkspace = (IWorkspaceResource)workspace )
.as(IWorkspaceResource.class);
// retrieve the RIDs of all checked out resources
IRidSet rids = myWorkspace.getCheckOutSet();
// check the resources in again
IRidIterator iterator = rids.iterator();
while( iterator.hasNext() ) {
IResource resource = ResourceFactory.getInstance()
.getResource(iterator.next(), context);
resource.checkIn(null, null);
}
Automatic versioning is not currently set by the client. The IVersionControlledResource interface offers
getAutoVersioningMode() for retrieving the auto versioning mode as provided by the repository for the given resource.
If automatic version control (see RF Concepts) is available for a collection, it can be "casted" to an IVersionController semantic object
that offers the necessary methods enable(), disable() and isEnabled() for enabling, disabling, or retrieving the
automatic version control flag for the given collection.
Version controlled collections (see RF Concepts) are created by simply enabling version control on an ICollection instead of an
IResource.