Java tutorial
/* Copyright (C) 2007-2009 Helge Hess This file is part of Go. Go is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. Go is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Go; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.getobjects.ofs; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.security.auth.login.Configuration; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.getobjects.appserver.core.WOResourceManager; import org.getobjects.appserver.publisher.IGoAuthenticator; import org.getobjects.appserver.publisher.IGoAuthenticatorContainer; import org.getobjects.appserver.publisher.IGoContext; import org.getobjects.appserver.publisher.IGoObject; import org.getobjects.appserver.publisher.IGoSecuredObject; import org.getobjects.appserver.publisher.IGoUser; import org.getobjects.appserver.publisher.GoAuthRequiredException; import org.getobjects.appserver.publisher.GoClass; import org.getobjects.appserver.publisher.GoContainerResourceManager; import org.getobjects.appserver.publisher.GoHTTPAuthenticator; import org.getobjects.appserver.publisher.GoRole; import org.getobjects.appserver.publisher.GoSessionAuthenticator; import org.getobjects.eocontrol.EODataSource; import org.getobjects.foundation.UObject; import org.getobjects.foundation.UString; import org.getobjects.ofs.config.GoConfigContext; import org.getobjects.ofs.config.GoConfigKeys; import org.getobjects.ofs.config.GoConfigProcessor; import org.getobjects.ofs.config.GoConfigKeys.KeyMatchEntry; import org.getobjects.ofs.fs.IOFSFileInfo; import org.getobjects.ofs.fs.IOFSFileManager; /** * OFSFolder * <p> * The OFSFolder is a central object in OFS: it manages configurations, * security, object caches, a resource manager, etc - on a per folder * basis. */ public class OFSFolder extends OFSBaseObject implements IGoFolderish, IOFSLifecycleObject, IGoSecuredObject, IGoAuthenticatorContainer { // TBD: document class protected static final Log cfglog = LogFactory.getLog("GoConfig"); protected static final Log authlog = LogFactory.getLog("GoAuthenticator"); /** * This is a cache of the child names for the specific folder object (after * it got looked up in the <code>pathToChildInfo</code> hashmap. */ protected OFSFileContainerChildInfo childInfo; /** * This timestamp indicates the last time this folder was modified. * This information is necessary for deciding if caches can be used or need * to be rebuilt. */ protected long lastModified; /** * Here we cache objects which we found using lookupName(). Note that we only * use the lookup name as the lookup criterion. Subclasses might use * additional information, ie when doing context-specific lookups (eg per * user-agent lookups). */ protected Map<String, Object> cacheNameToObject; protected Map<String, Object> cacheNameToConfig; protected Object ownConfig; /** * The resource manager associated with the folder. */ protected WOResourceManager resourceManager; protected IGoContext context; /* required for the RM? */ /** * This method just grabs the <code>_ctx</code> early in the process. We need * it for the resourcemanager. * <p> * This is a lifecycle callback invoked by the restoration factory. */ public Object awakeFromRestoration(final OFSRestorationFactory _factory, final Object _container, final IOFSFileManager _fm, final IOFSFileInfo _file, final IGoContext _ctx) { // TBD: check whether we can remove the context from the controller. Its // currently required by the resource manager, I think during the // lookup phase (not 100% sure). this.context = _ctx; return this; } /* directory contents */ public OFSFileContainerChildInfo childInfo() { final IOFSFileInfo info = this.fileInfo(); if (info == null) return null; final long currentTimestamp = info.lastModified(); // need to rebuild caches? if (this.childInfo != null && currentTimestamp != this.lastModified) this.childInfo = null; if (this.childInfo == null) { final ConcurrentHashMap<IOFSFileInfo, Object> pathToChildInfo = this.fileManager .cacheForSection("OFSFolderChildInfo"); /* check cache */ this.childInfo = (OFSFileContainerChildInfo) pathToChildInfo.get(info); if (this.childInfo != null) { // Hm, this does not seem to speedup the operation, even though we get // a good hitrate? Maybe the kernel cache is sufficient or the File // does some caching? if (currentTimestamp != this.childInfo.timestamp()) { // no gain in removing the old info? Will be overridden below this.childInfo = null; } } /* fetch item if cache was empty or item got changed */ if (this.childInfo == null) { this.childInfo = OFSFileContainerChildInfo.infoForFile(this.fileManager(), this.fileInfo()); if (this.childInfo != null) { this.childInfo.load(); /* ensure a threadsafe state */ pathToChildInfo.put(info, this.childInfo); } } } return this.childInfo; } /* container */ public boolean isFolderish() { return true; /* not strictly necessary, but this is static info anyways */ } /* contents */ protected static final String[] emptyStringArray = new String[0]; protected String[] collectIds(final boolean _directories) { final OFSFileContainerChildInfo ci = this.childInfo(); if (ci == null) return null; final String[] fileNames = ci.fileNames(); int len = fileNames.length; if (len == 0) return emptyStringArray; final List<String> ids = new ArrayList<String>(8); for (int i = 0; i < len; i++) { final IOFSFileInfo info = this.fileManager.fileInfoForPath(this.storagePath, fileNames[i]); if (info.isDirectory() == _directories) ids.add(ci.ids[i]); } len = ids.size(); return len == 0 ? emptyStringArray : ids.toArray(new String[len]); } public String[] toOneRelationshipKeys() { return this.collectIds(false /* files */); } public String[] toManyRelationshipKeys() { return this.collectIds(true /* directories */); } public String[] objectIds() { final OFSFileContainerChildInfo ci = this.childInfo(); if (ci == null) return null; return ci != null ? ci.ids() : null; } /* IGoFolderish */ /** * The default implementation returns an OFSFolderDataSource focused on this * object. * * @return a datasource representing the contents of this folder */ public EODataSource folderDataSource(final IGoContext _ctx) { return new OFSFolderDataSource(this, _ctx); } /* stored keys */ /** * This method first locates the IOFSFileInfo for the given name in the * folder. It then uses the OFSRestorationFactory derived from the context * to reconstruct the child object. * <p> * This object does no caching of the resulting object. All caching is done * by the lookupName() method. * <p> * The method is called by lookupName(), you usually don't call it manually. * * @param _name - name of the object to lookup * @param _ctx - the context to perform the operation in * @return a freshly created object, or an Exception/null on error */ public Object lookupStoredName(final String _name, final IGoContext _ctx) { // Note: do not call configurationForNameInContext() in here, might result // in a cycle! (since the config is also looked up using the method) final IOFSFileInfo linfo = this.lookupInfoForName(_name, _ctx); if (linfo == null) return null; /* find factory using the context */ final OFSRestorationFactory factory = OFSRestorationFactory.restorationFactoryInContext(_ctx); if (factory == null) { if (log.isDebugEnabled()) log.debug("did not find OFS restoration factory!"); return null; } /* attempt to restore object */ final Object o = factory.restoreObjectFromFileInContext(this, this.fileManager, linfo, _ctx); if (log.isDebugEnabled()) { if (o != null) log.debug("restored OFS object: " + o); else log.debug("could not restore file: " + linfo); } return o; } /** * This method first locates the IOFSFileInfo for the given name in the * folder. * * @param _name - name of the object to lookup * @param _ctx - the context to perform the operation in * @return the IOFSFileInfo object, or null if the name could not be resolved */ public IOFSFileInfo lookupInfoForName(final String _name, final IGoContext _ctx) { final boolean debugOn = log.isDebugEnabled(); /* first turn lookup name into lookup id (aka: cut off extension */ final String lookupId = this.idFromName(_name, _ctx); if (debugOn) log.debug("lookupStoredName(" + _name + ") => id=" + lookupId); /* lookup File object for given id */ final OFSFileContainerChildInfo ci = this.childInfo(); if (ci == null) { if (debugOn) log.debug("did not find childinfo of container: " + this); return null; } else if (debugOn) log.debug(" childinfo: " + ci); final String[] files = ci.fileNames(); int len = files.length; if (len == 0) { if (debugOn) log.debug("childinfo of container returned no filenames: " + this); return null; } if (debugOn) log.debug(" number of files: " + files.length); String lfile = null; for (int i = 0; i < len; i++) { if (debugOn) log.debug(" check[" + i + "]: " + ci.fileIds[i]); if (lookupId.equals(ci.fileIds[i])) { lfile = files[i]; // TODO: DEBUG if (!files[i].startsWith(lookupId)) { log.error("FOUND " + lookupId + " as " + lfile); for (int j = 0; j < len; j++) { log.error(" id: " + ci.fileIds[j]); log.error(" =>: " + ci.fileNames[j]); } } break; } } if (lfile == null) { if (debugOn) log.debug("did not find file for id: " + lookupId); return null; } final IOFSFileInfo linfo = this.fileManager.fileInfoForPath(this.storagePath, lfile); if (debugOn) log.debug("found file for id=" + lookupId + " => " + lfile + ": " + linfo); return linfo; } /* IGoObject */ /** * Lookup the given name in this object. This works by first checking the * GoClass of the object and then calling lookupStoredName() to discover an * object on-disk. * <p> * This method maintains a cache of restored disk objects. * * @param _name - name of the object to lookup * @param _ctx - the context to perform the operation in * @param _acquire - whether the object should attempt to acquire names * @return a freshly created object, or an Exception/null on error */ @SuppressWarnings("unchecked") @Override public Object lookupName(final String _name, final IGoContext _ctx, final boolean _acquire) { final boolean debugOn = log.isDebugEnabled(); /* first check cache */ if (this.cacheNameToObject != null) { final Object o = this.cacheNameToObject.get(_name); if (o != null) { if (debugOn) log.debug("cache hit[" + _name + "]: " + o); return o; } if (debugOn) log.debug("cache miss[" + _name + "]."); } else if (debugOn) log.debug("no child cache in container: " + this); /* lookup using GoClass */ final GoClass cls = this.goClassInContext(_ctx); if (cls != null) { Object o = cls.lookupName(this, _name, _ctx); if (o != null) return o; } /* check configuration for replacement names */ final Map<String, ?> cfg = this.configurationInContext(_ctx); final List<KeyMatchEntry> aliases = (List<KeyMatchEntry>) (cfg != null ? cfg.get(GoConfigKeys.AliasMatchName) : null); if (aliases != null) { // TBD: bad, we grab AliasMatchEntry from htaccess if (debugOn) { log.debug("lookup '" + _name + "' process AliasMatchName: " + UString.componentsJoinedByString(aliases, ",")); log.debug(" in: " + this); } for (final KeyMatchEntry entry : aliases) { final String newName = entry.match(_name); if (newName != null && !newName.equals(_name)) { if (debugOn) log.debug(" match, rewrite '" + _name + "' to '" + newName + "'"); final Object o = this.lookupName(newName, _ctx, _acquire); if (o instanceof OFSBaseObject) { /* push *old* name as the (virtual) location of the replacement */ ((OFSBaseObject) o).setLocation(this, _name); } /* Cache replacement object under lookup name (already cached under * its own name) */ if (this.cacheNameToObject != null) this.cacheNameToObject.put(_name, o); return o; } } } /* check children */ final OFSFileContainerChildInfo ci = this.childInfo(); if (ci != null && ci.hasKey(_name)) { final Object o = this.lookupStoredName(_name, _ctx); if (o != null) { if (this.cacheNameToObject != null) this.cacheNameToObject.put(_name, o); return o; } } else if (log.isDebugEnabled()) { if (ci != null) log.debug("container misses key '" + _name + "' in: " + ci); else log.debug("container has no child info: " + this); } /* if we shall acquire, continue at parent */ if (_acquire && this.container != null) return ((IGoObject) this.container).lookupName(_name, _ctx, true /* aq */); return null; } /* IGoSecuredObject */ /** * This method checks the requirements stated in the configuration associated * with this object (usually declared in an config.htaccess file). * Its called by validateName() and validateObject(). * <p> * @param _requirements - the requirements to be checked * @param _ctx - the context containing the active user * @return null if the user has access, a GoSecurityException otherwise */ public Exception validateRequirements(final Map<String, Set<String>> _requirements, IGoContext _ctx) { if (_requirements == null || _requirements.size() == 0) return null; /* nothing to be done */ Set<String> requiredRoles = null; Set<String> requiredLogins = null; for (String requireType : _requirements.keySet()) { if (requireType.equals(GoConfigKeys.Require_ValidUser)) { if (requiredRoles == null) requiredRoles = new HashSet<String>(4); requiredRoles.add(GoRole.Authenticated); } else if (requireType.equals(GoConfigKeys.Require_Group)) { if (requiredRoles == null) requiredRoles = new HashSet<String>(4); requiredRoles.addAll(_requirements.get(GoConfigKeys.Require_Group)); } else if (requireType.equals(GoConfigKeys.Require_User)) { if (requiredLogins == null) requiredLogins = new HashSet<String>(4); requiredLogins.addAll(_requirements.get(GoConfigKeys.Require_User)); } else log.warn("not processing requirement: " + requireType); } if (requiredRoles == null && requiredLogins == null) { if (authlog.isInfoEnabled()) authlog.info("no requirements configured."); return null; /* nothing was required */ } /* check logins and roles against active user */ final IGoUser user = _ctx.activeUser(); if (user == null) authlog.warn("got no activeUser from ctx: " + _ctx); if (authlog.isInfoEnabled()) { authlog.info("checking against user: " + user + "\n" + " rq-logins: " + UString.componentsJoinedByString(requiredLogins, ",") + "\n" + " rq-roles: " + UString.componentsJoinedByString(requiredRoles, ",")); } /* first check whether the user is part of the required ones */ if (requiredLogins != null && requiredLogins.contains(user.getName())) { // TBD: check all principals of the subject? if (authlog.isInfoEnabled()) authlog.info(" user matched by login: " + user); return null; /* access is OK, requirements contain user name */ } /* next check whether the roles intersect (whether the user has a role * which is required) */ if (requiredRoles != null && requiredRoles.size() > 0) { // TBD: roles should include Group principals of the user subject? final String[] mainRoles = user != null ? user.rolesForObjectInContext(null, _ctx) : null; if (mainRoles != null) { for (String userRole : mainRoles) { if (requiredRoles.contains(userRole)) { if (authlog.isInfoEnabled()) authlog.info(" user matched by role: " + requiredRoles); return null; /* access is OK, user has a required role */ } } } else if (authlog.isInfoEnabled()) authlog.info("user has no roles configured, required: " + requiredRoles); } /* requirements check failed, raise an exception */ return new GoAuthRequiredException(this.authenticatorInContext(_ctx), "user does not match configured requirements: " + user != null ? user.getName() : "<null>"); } @SuppressWarnings("unchecked") public Exception validateName(final String _name, final IGoContext _ctx) { /* do not rerun validation on cached objects */ if (this.cacheNameToObject != null) { if (this.cacheNameToObject.containsKey(_name)) return null; } // TBD: Should this run for names which are not contained in the folder? Eg // aquired frame templates are a common situation. // Tricky, not sure yet what the proper thing is. // WELL: the PATH must be correct for acquired objects. Right now we just // add the _name to the PATH, hence it isn't correct for acquired // resources. // => I think we can only check LocationMatch in here? final OFSFileContainerChildInfo ci = this.childInfo(); if (ci == null || !ci.hasKey(_name)) { // TBD: but what about GoClass methods?! We need to be able to customize // the lookup of those final GoClass cls = this.goClassInContext(_ctx); if (cls != null) { Object o = cls.lookupName(this, _name, _ctx); if (o == null) return null; /* we do not provide the given name */ } else return null; /* we do not provide the given name */ } final Map<String, ?> cfg = this.configurationForNameInContext(_name, _ctx); if (cfglog.isDebugEnabled()) cfglog.debug("validateName('" + _name + "') with cfg: " + cfg); /* check configuration for requirements */ if (cfg != null) { final Exception error = this .validateRequirements((Map<String, Set<String>>) cfg.get(GoConfigKeys.Require), _ctx); if (error != null) { if (authlog.isInfoEnabled()) authlog.info("requirements failed", error); return error; } } if (authlog.isInfoEnabled()) authlog.info("requirements ok, continue ..."); /* also run the default implementation */ return IGoSecuredObject.DefaultImplementation.validateNameOfObject(this, _name, _ctx); } @SuppressWarnings("unchecked") public Exception validateObject(final IGoContext _ctx) { if (true) return null; // DOES NOT WORK YET final Map<String, ?> cfg = this.configurationInContext(_ctx); if (cfglog.isDebugEnabled()) { cfglog.debug("validate('" + this.getClass().getSimpleName() + "') with cfg: " + cfg); } /* check configuration for requirements */ if (cfg != null) { final Exception error = this .validateRequirements((Map<String, Set<String>>) cfg.get(GoConfigKeys.Require), _ctx); if (error != null) { if (authlog.isInfoEnabled()) authlog.info("requirements failed", error); return error; } } if (authlog.isInfoEnabled()) authlog.info("requirements ok, continue ..."); /* also run the default implementation */ return IGoSecuredObject.DefaultImplementation.validateObject(this, _ctx); } public Exception validatePermission(String _perm, final IGoContext _ctx) { return IGoSecuredObject.DefaultImplementation.validatePermissionOnObject(this, _perm, _ctx); } protected IGoAuthenticator cachedAuthenticator; /** * Returns an IGoAuthenticator managed by the folder. The default * implementation uses the 'configurationInContext()' to build the * authenticator. * * @param _ctx - the active IGoContext (usually the WOContext) * @return an authenticator, derived from the configuration */ public IGoAuthenticator authenticatorInContext(IGoContext _ctx) { if (this.cachedAuthenticator != null) return this.cachedAuthenticator; final Map<String, ?> cfg = this.configurationInContext(_ctx); if (cfg == null) return null; /* no configuration at all */ String authType = (String) cfg.get(GoConfigKeys.AuthType); if (UObject.isEmpty(authType)) return null; /* no AuthType configured */ // TBD: move to some generic Config=>Authenticator factory object String authName = (String) cfg.get(GoConfigKeys.AuthName); if ("Basic".equalsIgnoreCase(authType)) { Configuration jaasCfg = null; // TBD GoHTTPAuthenticator auth = new GoHTTPAuthenticator(authName, jaasCfg); return (this.cachedAuthenticator = auth); } if ("WOSession".equalsIgnoreCase(authType)) { /* Note: no JAAS is required, actual login is done in the LoginPage. The * session authenticator just checks the session for an active user, * if there is none, it returns the Anonymous user (which will usually * raise an Authentication exception). * Further the session-auth renders AuthExceptions as redirects to a * login page. */ final GoSessionAuthenticator auth = new GoSessionAuthenticator(); String s = (String) cfg.get("authloginpage"); if (UObject.isNotEmpty(s)) auth.setPageName(s); s = (String) cfg.get("authloginurl"); if (UObject.isNotEmpty(s)) auth.setRedirectURL(s); return (this.cachedAuthenticator = auth); } log.error("unsupported authenticator type: " + authType); return null; } /* configuration */ private static final Object CACHE_MISS = new Object(); /** * Returns the configuration for the folder itself. This is invoked from * various places, eg: * <ul> * <li>to determine the authenticator in authenticatorInContext() * <li>to check permissions of the folder in validateObject() * <li>to determine replacement objects in lookupName() * </ul> * * @param _ctx - the active GoContext * @return the configuration dictionary, or null if there was none */ @SuppressWarnings("unchecked") public Map<String, Object> configurationInContext(final IGoContext _ctx) { if (this.ownConfig != null) return this.ownConfig == CACHE_MISS ? null : (Map) this.ownConfig; if (_ctx instanceof GoConfigContext) return null; /* this got called during a config-lookup */ /* setup config context */ final GoConfigContext configContext = new GoConfigContext(_ctx, "location", this.pathInContainer(), "path", this.storagePath(), "filename", "", "dirpath", this.storagePath()); /* apply config */ final GoConfigProcessor cpu = new GoConfigProcessor(); final Object cfg = cpu.buildConfiguration(this, configContext); this.ownConfig = cfg != null ? cfg : CACHE_MISS; return this.ownConfig == CACHE_MISS ? null : (Map) this.ownConfig; } /** * Retrieves the OFS configuration for the given name. Note that the returned * configuration is relative to the folder, eg a subfolder could add * additional information to its own configuration. * <p> * Eg this is called by 'validateName()' to check for access restrictions. * * @param _name - the name of the object to retrieve configuration for * @param _ctx - the web transaction context * @return an Object representing the configuration for the name */ @SuppressWarnings("unchecked") public Map<String, ?> configurationForNameInContext(final String _name, final IGoContext _ctx) { // TBD: this only works for contained objects because the storagePath // depends on lookup! (could be acquired or remapped) Object cfg; if (this.cacheNameToConfig != null) { if ((cfg = this.cacheNameToConfig.get(_name)) != null) return cfg == CACHE_MISS ? null : (Map) cfg; } if (_ctx instanceof GoConfigContext) return null; /* this got called during a config-lookup */ /* setup config context */ String objId = this.idFromName(_name, _ctx); String[] childPath = UString.addStringToStringArray(this.storagePath, objId); String[] childLoc = UString.addStringToStringArray(this.pathInContainer(), _name); GoConfigContext configContext = new GoConfigContext(_ctx, "location", childLoc, "path", childPath, "filename", objId, "dirpath", this.storagePath()); /* apply config */ final GoConfigProcessor cpu = new GoConfigProcessor(); cfg = cpu.buildConfiguration(this, configContext); /* cache */ if (this.cacheNameToConfig == null) this.cacheNameToConfig = new HashMap<String, Object>(16); this.cacheNameToConfig.put(_name, cfg != null ? cfg : CACHE_MISS); return (Map) cfg; } /** * Lookup and cache a resource manager for the folder. * * @return a WOResourceManager instance */ public WOResourceManager resourceManager() { if (this.resourceManager != null) return this.resourceManager; final WOResourceManager parentRM = GoContainerResourceManager.lookupResourceManager(this.container(), this.context); this.resourceManager = new GoContainerResourceManager(this, parentRM, this.context); return this.resourceManager; } /* description */ @Override public void appendAttributesToDescription(final StringBuilder _d) { super.appendAttributesToDescription(_d); if (this.childInfo != null) _d.append(" has-childinfo"); } }