/* * $Id$ * * Copyright 2006 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ /*------------------------------------------------------------------------------ * * Written by: Josh Moore <josh.moore@gmx.de> * *------------------------------------------------------------------------------ */ package ome.logic; import java.io.File; import java.io.FileReader; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import ome.annotations.PermitAll; import ome.annotations.RolesAllowed; import ome.api.IConfig; import ome.api.ServiceInterface; import ome.api.local.LocalConfig; import ome.conditions.ApiUsageException; import ome.conditions.InternalException; import ome.conditions.OptimisticLockException; import ome.conditions.SecurityViolation; import ome.security.basic.CurrentDetails; import ome.services.db.DatabaseIdentity; import ome.system.PreferenceContext; import ome.util.SqlAction; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.transaction.annotation.Transactional; /** * implementation of the IConfig service interface. * * Also used as the main developer example for developing (stateless) ome.logic * implementations. See source code documentation for more. * * @author Josh Moore, josh.moore at gmx.de * @version $Revision$, $Date$ * @since 3.0-M3 * @see IConfig */ /* * Developer notes: --------------- The two annotations below are activated by * setting the subversion properties on this class file. They can be accessed * via ome.system.Version */ /* * Developer notes: --------------- The annotations below (and on the individual * methods) are central to the definition of this service. They are used in * place of XML configuration files (though the XML deployment descriptors, as * they are called, can be used to override the annotations), and will influence */ // ~ Service annotations // ============================================================================= /* * Source: Spring Purpose: Used by EventHandler#checkReadyOnly(MethodInvocation) * to deteremine if a method is read-only. No annotation implies ready-only, so * it is essential to have this annotation on all write methods. */ @Transactional /* * Stateless. This class implements ServiceInterface but not * StatefulServiceInterface making it stateless. This means that the entire * server will most likely only contain one of these instances. No mutable * fields should be present unlessvery carefully synchronized. * * Local configurations are not exposed to clients, and are typically only used * within a server instance. */ public class ConfigImpl extends AbstractLevel2Service implements LocalConfig { /* * Stateful differences: -------------------- A stateful service must be * marked as Serializable and all fields must be either marked transient, be * serializable themselves, or be set to null before serialization. Here * we've marked the jdbc field as transient out of habit. * * @see http://trac.openmicroscopy.org.uk/ome/ticket/173 */ private transient SqlAction sql; private transient PreferenceContext prefs; private transient CurrentDetails currentDetails; private transient DatabaseIdentity db; /** * Protects all access to the configuration properties. */ private final transient ReadWriteLock lock = new ReentrantReadWriteLock(); /** * {@link SqlAction} setter for dependency injection. * * @param sql the SQL action instance * @see ome.services.util.BeanHelper#throwIfAlreadySet(Object, Object) */ /* * Developer notes: --------------- Because of the complicated lifecycle of * EJBs it is not possible to fully configure them with constructor * injection (which is safer). Instead, we have to provide public setters * for all properties which need to be injected. And since Java doesn't have * the concept of "friends" (yet), this opens up our classes for some weird * manipulations. Therefore we've made all bean setters "final" and added a * call to "throwIfAlreadySet" which will only allow previously null fields * to be set. */ public final void setSqlAction(SqlAction sql) { getBeanHelper().throwIfAlreadySet(this.sql, sql); this.sql = sql; } /** * {@link PreferenceContext} setter for dependency injection. * * @param prefs the preference context * @see ome.services.util.BeanHelper#throwIfAlreadySet(Object, Object) */ public final void setPreferenceContext(PreferenceContext prefs) { getBeanHelper().throwIfAlreadySet(this.prefs, prefs); this.prefs = prefs; } /** * {@link CurrentDetails} setter for dependency injection. * * @param currentDetails the details of the current thread's security context * @see ome.services.util.BeanHelper#throwIfAlreadySet(Object, Object) */ public final void setCurrentDetails(CurrentDetails currentDetails) { getBeanHelper().throwIfAlreadySet(this.currentDetails, currentDetails); this.currentDetails = currentDetails; } public final void setDatabaseIdentity(DatabaseIdentity db) { this.db = db; } /* * Developer notes: --------------- This method provides the lookup value * needed for finding services within the Spring context and, by convention, * the value which is to be returned can be found in the file * "ome/services/service-<class name>.xml" */ public final Class<? extends ServiceInterface> getServiceInterface() { return IConfig.class; } // ~ Service methods // ========================================================================= /* * Source: ome.annotations package * * Purpose: as opposed to RolesAllowed (below), permits this method to be * called by anyone, regardless of their group membership. As of the move to * OmeroBlitz, the user will still have to have created a session to gain * access (unlike under JavaEE). */ /** * see {@link IConfig#getServerTime()} */ @PermitAll public Date getServerTime() { return new Date(); } /** * see {@link IConfig#getDatabaseTime()} */ @PermitAll // see above public Date getDatabaseTime() { Date date = sql.now(); return date; } /* * Source: ome.annotations package * * Purpose: defines the role which must have been obtained during * authentication and authorization in order to access this method. This * works in combination with the BasicMethodSecurity to fully define * security semantics. */ /** * see {@link IConfig#getConfigValue(String)} */ @PermitAll // see above public String getConfigValue(String key) { if (key == null) { return ""; } key = prefs.resolveAlias(key); if (!prefs.canRead(currentDetails.getCurrentEventContext(), key)) { throw new SecurityViolation("Cannot read configuration: " + key); } return getInternalValue(key); } @PermitAll public Map<String, String> getConfigValues(String keyRegex) { if (keyRegex == null) { return Collections.emptyMap(); } Pattern p = Pattern.compile(keyRegex); Map<String, String> rv = new HashMap<String, String>(); Set<String> keys = prefs.getKeySet(); // Not resolving aliases since these come straight-from the prefs for (String key : keys) { if (p.matcher(key).find()) { if (prefs.canRead( currentDetails.getCurrentEventContext(), key)) { rv.put(key, getInternalValue(key)); } } } return rv; } @RolesAllowed("system") public Map<String, String> getConfigDefaults() { File etc = new File("etc"); File omero = new File(etc, "omero.properties"); Properties p = new Properties(); Map<String, String> rv = new HashMap<String, String>(); try { FileReader r = new FileReader(omero); p.load(r); for (Entry<Object, Object> entry : p.entrySet()) { rv.put(entry.getKey().toString(), entry.getValue().toString()); } return rv; } catch (Exception e) { InternalException ie = new InternalException(e.getMessage()); ie.initCause(e); throw ie; } } @PermitAll public Map<String, String> getClientConfigValues() { return getConfigValues("^omero\\.client\\."); } @PermitAll public Map<String, String> getClientConfigDefaults() { Map<String, String> rv = getConfigDefaults(); Map<String, String> copy = new HashMap<String, String>(); for (Map.Entry<String, String> e : rv.entrySet()) { if (e.getKey().startsWith("omero.client")) { copy.put(e.getKey(), e.getValue()); } } return copy; } public String getInternalValue(String key) { key = prefs.resolveAlias(key); lock.readLock().lock(); try { String value = null; if (prefs.checkDatabase(key)) { value = fromDatabase(key); } if (value != null) { return value; } else { return prefs.getProperty(key); } } finally { lock.readLock().unlock(); } } /** * see {@link IConfig#setConfigValue(String, String)} */ @RolesAllowed("system") // see above public void setConfigValue(String key, String value) { key = prefs.resolveAlias(key); lock.writeLock().lock(); try { boolean set = false; // If the value comes from the db, then set it there and return if (prefs.checkDatabase(key)) { String current = fromDatabase(key); if (current != null && current.length() > 0) { int count = sql.updateConfiguration(key, value); if (count != 1) { throw new OptimisticLockException( "Configuration tabled during modification of : " + key); } set = true; } } // Otherwise set it in the preferences. if (!set) { // With #800, we will need to raise some form of event here // to notify consumers. Also, this will be a temporary value // not saved on restart nor shared across JVMs. System.setProperty(key, value); } } finally { lock.writeLock().unlock(); } } /** * see {@link IConfig#setConfigValueIfEquals(String, String, String)} */ @RolesAllowed("user") // see above public boolean setConfigValueIfEquals(String key, String value, String test) throws ApiUsageException, SecurityViolation { key = prefs.resolveAlias(key); lock.writeLock().lock(); try { String current = getInternalValue(key); if (test == null) { if (current != null) { return false; } } else { if (!test.equals(current)) { return false; } } setConfigValue(key, value); return true; } finally { lock.writeLock().unlock(); } } /** * see {@link IConfig#getVersion()} */ @PermitAll // see above public String getVersion() { String version = getInternalValue("omero.version"); Pattern p = Pattern.compile(VERSION_REGEX); Matcher m = p.matcher(version); if (!m.matches()) { throw new InternalException("Bad version format:" + version); } return m.group(1); } @PermitAll // see above public String getDatabaseVersion() { return sql.dbVersion(); } @PermitAll // see above public String getDatabaseUuid() { return db.getUuid(); } // Helpers // ========================================================================= private String fromDatabase(String key) { String value = null; try { value = sql.configValue(key); } catch (EmptyResultDataAccessException erdae) { // ok returning null } return value; } }