blob: 219ec9e84e8bc7bbb06bb5309452e2dbc37015a3 [file] [log] [blame]
/*
* Copyright (c) OSGi Alliance (2004, 2010). All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.osgi.service.application;
import java.lang.reflect.Array;
import java.util.*;
import java.util.Map.Entry;
import org.eclipse.equinox.internal.app.AppPersistence;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
/**
* An OSGi service that represents an installed application and stores
* information about it. The application descriptor can be used for instance
* creation.
*
* @version $Revision: 1.13 $
*/
public abstract class ApplicationDescriptor {
/*
* NOTE: An implementor may also choose to replace this class in
* their distribution with a class that directly interfaces with the
* org.osgi.service.application implementation. This replacement class MUST NOT alter the
* public/protected signature of this class.
*/
/**
* The property key for the localized name of the application.
*/
public static final String APPLICATION_NAME = "application.name";
/**
* The property key for the localized icon of the application.
*/
public static final String APPLICATION_ICON = "application.icon";
/**
* The property key for the unique identifier (PID) of the application.
*/
public static final String APPLICATION_PID = Constants.SERVICE_PID;
/**
* The property key for the version of the application.
*/
public static final String APPLICATION_VERSION = "application.version";
/**
* The property key for the name of the application vendor.
*/
public static final String APPLICATION_VENDOR = Constants.SERVICE_VENDOR;
/**
* The property key for the visibility property of the application.
*/
public static final String APPLICATION_VISIBLE = "application.visible";
/**
* The property key for the launchable property of the application.
*/
public static final String APPLICATION_LAUNCHABLE = "application.launchable";
/**
* The property key for the locked property of the application.
*/
public static final String APPLICATION_LOCKED = "application.locked";
/**
* The property key for the localized description of the application.
*/
public static final String APPLICATION_DESCRIPTION = "application.description";
/**
* The property key for the localized documentation of the application.
*/
public static final String APPLICATION_DOCUMENTATION = "application.documentation";
/**
* The property key for the localized copyright notice of the application.
*/
public static final String APPLICATION_COPYRIGHT = "application.copyright";
/**
* The property key for the localized license of the application.
*/
public static final String APPLICATION_LICENSE = "application.license";
/**
* The property key for the application container of the application.
*/
public static final String APPLICATION_CONTAINER = "application.container";
/**
* The property key for the location of the application.
*/
public static final String APPLICATION_LOCATION = "application.location";
private final String pid;
private final boolean[] locked = {false};
/**
* Constructs the {@code ApplicationDescriptor}.
*
* @param applicationId
* The identifier of the application. Its value is also available
* as the {@code service.pid} service property of this
* {@code ApplicationDescriptor} service. This parameter must not
* be {@code null}.
* @throws NullPointerException if the specified {@code applicationId} is null.
*/
protected ApplicationDescriptor(String applicationId) {
if (null == applicationId) {
throw new NullPointerException("Application ID must not be null!");
}
this.pid = applicationId;
locked[0] = isPersistentlyLocked();
}
/**
* Returns the identifier of the represented application.
*
* @return the identifier of the represented application
*/
public final String getApplicationId() {
return pid;
}
/**
* This method verifies whether the specified {@code pattern}
* matches the Distinguished Names of any of the certificate chains
* used to authenticate this application.
* <P>
* The {@code pattern} must adhere to the
* syntax defined in {@link org.osgi.service.application.ApplicationAdminPermission}
* for signer attributes.
* <p>
* This method is used by {@link ApplicationAdminPermission#implies(java.security.Permission)} method
* to match target {@code ApplicationDescriptor} and filter.
*
* @param pattern a pattern for a chain of Distinguished Names. It must not be null.
* @return {@code true} if the specified pattern matches at least
* one of the certificate chains used to authenticate this application
* @throws NullPointerException if the specified {@code pattern} is null.
* @throws IllegalStateException if the application descriptor was
* unregistered
*/
public abstract boolean matchDNChain(String pattern);
/**
* Returns the properties of the application descriptor as key-value pairs.
* The return value contains the locale aware and unaware properties as
* well. The returned {@code Map} will include the service
* properties of this {@code ApplicationDescriptor} as well.
* <p>
* This method will call the {@code getPropertiesSpecific} method
* to enable the container implementation to insert application model and/or
* container implementation specific properties.
* <P>
* The returned {@link java.util.Map} will contain the standard OSGi service
* properties as well
* (e.g. service.id, service.vendor etc.) and specialized application
* descriptors may offer further service properties. The returned Map contains
* a snapshot of the properties. It will not reflect further changes in the
* property values nor will the update of the Map change the corresponding
* service property.
*
* @param locale
* the locale string, it may be null, the value null means the
* default locale. If the provided locale is the empty String
* ({@code ""})then raw (non-localized) values are returned.
*
* @return copy of the service properties of this application descriptor service,
* according to the specified locale. If locale is null then the
* default locale's properties will be returned. (Since service
* properties are always exist it cannot return null.)
*
* @throws IllegalStateException
* if the application descriptor is unregistered
*/
public final Map getProperties(String locale) {
Map props = getPropertiesSpecific(locale);
Boolean containerLocked = (Boolean) props.remove(APPLICATION_LOCKED);
synchronized (locked) {
if (containerLocked != null && containerLocked.booleanValue() != locked[0]) {
if (locked[0])
lockSpecific();
else
unlockSpecific();
}
}
/* replace the container's lock with the application model's lock, that's the correct */
props.put(APPLICATION_LOCKED, Boolean.valueOf(locked[0]));
return props;
}
/**
* Container implementations can provide application model specific
* and/or container implementation specific properties via this
* method.
*
* Localizable properties must be returned localized if the provided
* {@code locale} argument is not the empty String. The value
* {@code null} indicates to use the default locale, for other
* values the specified locale should be used.
*
* The returned {@link java.util.Map} must contain the standard OSGi service
* properties as well
* (e.g. service.id, service.vendor etc.) and specialized application
* descriptors may offer further service properties.
* The returned {@code Map}
* contains a snapshot of the properties. It will not reflect further changes in the
* property values nor will the update of the Map change the corresponding
* service property.
* @param locale the locale to be used for localizing the properties.
* If {@code null} the default locale should be used. If it is
* the empty String ({@code ""}) then raw (non-localized) values
* should be returned.
*
* @return the application model specific and/or container implementation
* specific properties of this application descriptor.
*
* @throws IllegalStateException
* if the application descriptor is unregistered
*/
protected abstract Map getPropertiesSpecific(String locale);
/**
* Launches a new instance of an application. The {@code args} parameter
* specifies the startup parameters for the instance to be launched, it may
* be null.
* <p>
* The following steps are made:
* <UL>
* <LI>Check for the appropriate permission.
* <LI>Check the locking state of the application. If locked then throw an
* {@link ApplicationException} with the reason code
* {@link ApplicationException#APPLICATION_LOCKED}.
* <LI>Calls the {@code launchSpecific()} method to create and start an
* application instance.
* <LI>Returns the {@code ApplicationHandle} returned by the
* launchSpecific()
* </UL>
* The caller has to have ApplicationAdminPermission(applicationPID,
* "launch") in order to be able to perform this operation.
* <P>
* The {@code Map} argument of the launch method contains startup arguments
* for the application. The keys used in the Map must be non-null, non-empty
* {@code String} objects. They can be standard or application specific.
* OSGi defines the {@code org.osgi.triggeringevent} key to be used to pass
* the triggering event to a scheduled application, however in the future it
* is possible that other well-known keys will be defined. To avoid unwanted
* clashes of keys, the following rules should be applied:
* <ul>
* <li>The keys starting with the dash (-) character are application
* specific, no well-known meaning should be associated with them.</li>
* <li>Well-known keys should follow the reverse domain name based naming.
* In particular, the keys standardized in OSGi should start with
* {@code org.osgi.}.</li>
* </ul>
* <P>
* The method is synchronous, it return only when the application instance
* was successfully started or the attempt to start it failed.
* <P>
* This method never returns {@code null}. If launching an application
* fails, the appropriate exception is thrown.
*
* @param arguments Arguments for the newly launched application, may be
* null
*
* @return the registered ApplicationHandle, which represents the newly
* launched application instance. Never returns {@code null}.
*
* @throws SecurityException if the caller doesn't have "lifecycle"
* ApplicationAdminPermission for the application.
* @throws ApplicationException if starting the application failed
* @throws IllegalStateException if the application descriptor is
* unregistered
* @throws IllegalArgumentException if the specified {@code Map} contains
* invalid keys (null objects, empty {@code String} or a key that is
* not {@code String})
*/
public final ApplicationHandle launch(Map arguments) throws ApplicationException {
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkPermission(new ApplicationAdminPermission(this, ApplicationAdminPermission.LIFECYCLE_ACTION));
synchronized (locked) {
if (locked[0])
throw new ApplicationException(ApplicationException.APPLICATION_LOCKED, "Application is locked, can't launch!");
}
if (!isLaunchableSpecific())
throw new ApplicationException(ApplicationException.APPLICATION_NOT_LAUNCHABLE, "Cannot launch the application!");
checkArgs(arguments, false);
try {
return launchSpecific(arguments);
} catch (IllegalStateException | SecurityException | ApplicationException ise) {
throw ise;
} catch (Exception t) {
throw new ApplicationException(ApplicationException.APPLICATION_INTERNAL_ERROR, t);
}
}
/**
* Called by launch() to create and start a new instance in an application
* model specific way. It also creates and registeres the application handle
* to represent the newly created and started instance and registeres it.
* The method is synchronous, it return only when the application instance was
* successfully started or the attempt to start it failed.
* <P>
* This method must not return {@code null}. If launching the application
* failed, and exception must be thrown.
*
* @param arguments
* the startup parameters of the new application instance, may be
* null
*
* @return the registered application model
* specific application handle for the newly created and started
* instance.
*
* @throws IllegalStateException
* if the application descriptor is unregistered
* @throws Exception
* if any problem occurs.
*/
protected abstract ApplicationHandle launchSpecific(Map arguments) throws Exception;
/**
* This method is called by launch() to verify that according to the
* container, the application is launchable.
*
* @return true, if the application is launchable according to the
* container, false otherwise.
*
* @throws IllegalStateException
* if the application descriptor is unregistered
*/
protected abstract boolean isLaunchableSpecific();
/**
* Schedules the application at a specified event. Schedule information
* should not get lost even if the framework or the device restarts so it
* should be stored in a persistent storage. The method registers a
* {@link ScheduledApplication} service in Service Registry, representing
* the created schedule.
* <p>
* The {@code Map} argument of the method contains startup arguments for the
* application. The keys used in the Map must be non-null, non-empty
* {@code String} objects. The argument values must be of primitive types,
* wrapper classes of primitive types, {@code String} or arrays or
* collections of these.
* <p>
* The created schedules have a unique identifier within the scope of this
* {@code ApplicationDescriptor}. This identifier can be specified in the
* {@code scheduleId} argument. If this argument is {@code null}, the
* identifier is automatically generated.
*
* @param scheduleId the identifier of the created schedule. It can be
* {@code null}, in this case the identifier is automatically
* generated.
* @param arguments the startup arguments for the scheduled application, may
* be null
* @param topic specifies the topic of the triggering event, it may contain
* a trailing asterisk as wildcard, the empty string is treated as
* "*", must not be null
* @param eventFilter specifies and LDAP filter to filter on the properties
* of the triggering event, may be null
* @param recurring if the recurring parameter is false then the application
* will be launched only once, when the event firstly occurs. If the
* parameter is true then scheduling will take place for every event
* occurrence; i.e. it is a recurring schedule
*
* @return the registered scheduled application service
*
* @throws NullPointerException if the topic is {@code null}
* @throws InvalidSyntaxException if the specified {@code eventFilter} is
* not syntactically correct
* @throws ApplicationException if the schedule couldn't be created. The
* possible error codes are
* <ul>
* <li>
* {@link ApplicationException#APPLICATION_DUPLICATE_SCHEDULE_ID} if
* the specified {@code scheduleId} is already used for this
* {@code ApplicationDescriptor} <li>
* {@link ApplicationException#APPLICATION_SCHEDULING_FAILED} if the
* scheduling failed due to some internal reason (e.g. persistent
* storage error). <li>
* {@link ApplicationException#APPLICATION_INVALID_STARTUP_ARGUMENT}
* if the specified startup argument doesn't satisfy the type or
* value constraints of startup arguments.
* </ul>
* @throws SecurityException if the caller doesn't have "schedule"
* ApplicationAdminPermission for the application.
* @throws IllegalStateException if the application descriptor is
* unregistered
* @throws IllegalArgumentException if the specified {@code Map} contains
* invalid keys (null objects, empty {@code String} or a key that is
* not {@code String})
*/
public final ScheduledApplication schedule(String scheduleId, Map arguments, String topic, String eventFilter, boolean recurring) throws InvalidSyntaxException, ApplicationException {
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkPermission(new ApplicationAdminPermission(this, ApplicationAdminPermission.SCHEDULE_ACTION));
arguments = checkArgs(arguments, true);
isLaunchableSpecific(); // checks if the ApplicationDescriptor was already unregistered
return AppPersistence.addScheduledApp(this, scheduleId, arguments, topic, eventFilter, recurring);
}
/**
* Sets the lock state of the application. If an application is locked then
* launching a new instance is not possible. It does not affect the already
* launched instances.
*
* @throws SecurityException
* if the caller doesn't have "lock" ApplicationAdminPermission
* for the application.
* @throws IllegalStateException
* if the application descriptor is unregistered
*/
public final void lock() {
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkPermission(new ApplicationAdminPermission(this, ApplicationAdminPermission.LOCK_ACTION));
synchronized (locked) {
if (locked[0])
return;
locked[0] = true;
lockSpecific();
saveLock(true);
}
}
/**
* This method is used to notify the container implementation that the
* corresponding application has been locked and it should update the
* {@code application.locked} service property accordingly.
* @throws IllegalStateException
* if the application descriptor is unregistered
*/
protected abstract void lockSpecific();
/**
* Unsets the lock state of the application.
*
* @throws SecurityException
* if the caller doesn't have "lock" ApplicationAdminPermission
* for the application.
* @throws IllegalStateException
* if the application descriptor is unregistered
*/
public final void unlock() {
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkPermission(new ApplicationAdminPermission(this, ApplicationAdminPermission.LOCK_ACTION));
synchronized (locked) {
if (!locked[0])
return;
locked[0] = false;
unlockSpecific();
saveLock(false);
}
}
/**
* This method is used to notify the container implementation that the
* corresponding application has been unlocked and it should update the
* {@code application.locked} service property accordingly.
* @throws IllegalStateException
* if the application descriptor is unregistered
*/
protected abstract void unlockSpecific();
private void saveLock(boolean locked) {
AppPersistence.saveLock(this, locked);
}
private boolean isPersistentlyLocked() {
return AppPersistence.isLocked(this);
}
private static final Collection scalars = Arrays.asList(new Class[] {String.class, Integer.class, Long.class, Float.class, Double.class, Byte.class, Short.class, Character.class, Boolean.class});
private static final Collection scalarsArrays = Arrays.asList(new Class[] {String[].class, Integer[].class, Long[].class, Float[].class, Double[].class, Byte[].class, Short[].class, Character[].class, Boolean[].class});
private static final Collection primitiveArrays = Arrays.asList(new Class[] {long[].class, int[].class, short[].class, char[].class, byte[].class, double[].class, float[].class, boolean[].class});
private static Map checkArgs(Map arguments, boolean validateValues) throws ApplicationException {
if (arguments == null)
return arguments;
Map copy = validateValues ? new HashMap() : null;
for (Iterator entries = arguments.entrySet().iterator(); entries.hasNext();) {
Map.Entry entry = (Entry) entries.next();
if (!(entry.getKey() instanceof String))
throw new IllegalArgumentException("Invalid key type: " + entry.getKey() == null ? "<null>" : entry.getKey().getClass().getName());
if ("".equals(entry.getKey())) //$NON-NLS-1$
throw new IllegalArgumentException("Empty string is an invalid key");
if (validateValues)
validateValue(entry, copy);
}
return validateValues ? copy : arguments;
}
private static void validateValue(Map.Entry entry, Map copy) throws ApplicationException {
Class clazz = entry.getValue().getClass();
// Is it in the set of scalar types
if (scalars.contains(clazz)) {
copy.put(entry.getKey(), entry.getValue());
return;
}
// Is it an array of primitives or scalars
if (scalarsArrays.contains(clazz) || primitiveArrays.contains(clazz)) {
int arrayLength = Array.getLength(entry.getValue());
Object copyOfArray = Array.newInstance(entry.getValue().getClass().getComponentType(), arrayLength);
System.arraycopy(entry.getValue(), 0, copyOfArray, 0, arrayLength);
copy.put(entry.getKey(), copyOfArray);
return;
}
// Is it a Collection of scalars
if (entry.getValue() instanceof Collection) {
Collection valueCollection = (Collection) entry.getValue();
for (Iterator it = valueCollection.iterator(); it.hasNext();) {
Class containedClazz = it.next().getClass();
if (!scalars.contains(containedClazz)) {
throw new ApplicationException(ApplicationException.APPLICATION_INVALID_STARTUP_ARGUMENT, "The value for key \"" + entry.getKey() + "\" is a collection that contains an invalid value of type \"" + containedClazz.getName() + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
copy.put(entry.getKey(), new ArrayList((Collection) entry.getValue()));
return;
}
throw new ApplicationException(ApplicationException.APPLICATION_INVALID_STARTUP_ARGUMENT, "The value for key \"" + entry.getKey() + "\" is an invalid type \"" + clazz.getName() + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}