/*==========================================================================*\
| $Id: AuthenticationDomain.java,v 1.7 2012/05/16 13:31:34 stedwar2 Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2006-2012 Virginia Tech
|
| This file is part of Web-CAT.
|
| Web-CAT is free software; you can redistribute it and/or modify
| it under the terms of the GNU Affero General Public License as published
| by the Free Software Foundation; either version 3 of the License, or
| (at your option) any later version.
|
| Web-CAT 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 General Public License for more details.
|
| You should have received a copy of the GNU Affero General Public License
| along with Web-CAT; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/
package org.webcat.core;
import com.webobjects.foundation.*;
import com.webobjects.eoaccess.*;
import com.webobjects.eocontrol.*;
import er.extensions.foundation.*;
import java.io.File;
import java.util.*;
import org.webcat.core.Application;
import org.webcat.core.AuthenticationDomain;
import org.webcat.core.UserAuthenticator;
import org.webcat.core.WCProperties;
import org.webcat.core._AuthenticationDomain;
import org.webcat.woextensions.ECAction;
import static org.webcat.woextensions.ECAction.run;
import org.apache.log4j.Logger;
// -------------------------------------------------------------------------
/**
* Represents an institution or organization, and an associated authentication
* policy. Some institutions may have multiple AuthenticationDomain objects
* that each represent a different authentication policy/mechanism for
* different classes of user names.
*
* @author Stephen Edwards
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.7 $, $Date: 2012/05/16 13:31:34 $
*/
public class AuthenticationDomain
extends _AuthenticationDomain
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new AuthenticationDomain object.
*/
public AuthenticationDomain()
{
super();
}
//~ Constants .............................................................
public static final String COOKIE_LAST_USED_INSTITUTION =
"org.webcat.core.AuthenticationDomain.lastUsed";
//~ Methods ...............................................................
// ----------------------------------------------------------
/**
* Get a human-readable representation of this authenticator, which is
* the same as {@link #name()}.
* @return this authenticator's property name
*/
public String userPresentableDescription()
{
return name();
}
// ----------------------------------------------------------
/**
* Change the value of this object's <code>propertyName</code>
* property. Takes care of renaming the associated subdirectories
* for this domain as well.
*
* @param value The new value for this property
*/
public void setPropertyName(String value)
{
if (value != null && propertyName() != null
&& !value.equals( propertyName()))
{
String oldSubdir = subdirName();
cachedName = null;
cachedSubdirName = null;
super.setPropertyName(value);
String newSubdir = subdirName();
// rename the three key directories
{
File parent = new File(Application.configurationProperties()
.getProperty("grader.submissiondir"));
File oldDir = new File(parent, oldSubdir);
oldDir.renameTo(new File(parent, newSubdir));
}
{
File parent = new File(Application.configurationProperties()
.getProperty("grader.workarea"));
File oldDir = new File(parent, oldSubdir);
oldDir.renameTo(new File(parent, newSubdir));
}
{
String scriptRoot = Application.configurationProperties()
.getProperty("grader.scriptsroot");
if (scriptRoot == null)
{
scriptRoot = Application.configurationProperties()
.getProperty("grader.submissiondir")
+ "/UserScripts";
}
File parent = new File(scriptRoot);
File oldDir = new File(parent, oldSubdir);
oldDir.renameTo(new File(parent, newSubdir));
String scriptDataRoot = Application.configurationProperties()
.getProperty("grader.scriptsdataroot");
if (scriptDataRoot == null)
{
scriptDataRoot = scriptRoot + "Data";
}
parent = new File(scriptDataRoot);
oldDir = new File(parent, oldSubdir);
oldDir.renameTo(new File(parent, newSubdir));
}
}
else
{
cachedSubdirName = null;
super.setPropertyName(value);
}
}
// ----------------------------------------------------------
/**
* Retrieve the user authenticator associated with this domain.
* @return an authenticator object to use when validating credentials
* for users in this authentication domain
*/
public UserAuthenticator authenticator()
{
return theAuthenticatorMap.get(propertyName());
}
// ----------------------------------------------------------
/**
* Get the name of this authenticator (the property name, without the
* "authenticator." prefix).
* @return the name as a string
*/
public String name()
{
if (cachedName == null)
{
String name = propertyName();
final String propertyPrefix = "authenticator.";
if (name != null)
{
// strip the prefix on the property name
if (name.startsWith(propertyPrefix))
{
name = name.substring(propertyPrefix.length());
}
}
cachedName = name;
}
return cachedName;
}
// ----------------------------------------------------------
/**
* Generate a name usable as a subdirectory name from this
* objects property name.
* @return a subdirectory name
*/
public String subdirName()
{
if (cachedSubdirName == null)
{
cachedSubdirName = subdirNameOf(name());
}
return cachedSubdirName;
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where all submissions for any
* user in this authentication domain are stored.
* @return the directory name as a string buffer, so that additional
* subdir components can be appended easily.
*/
public StringBuffer submissionBaseDirBuffer()
{
StringBuffer dir = new StringBuffer(50);
// Strictly speaking, this info is associated with the Grader
// subsystem and should be located there, but it has leaked into
// core because it is needed in too many places.
dir.append(Application.configurationProperties()
.getProperty("grader.submissiondir"));
dir.append('/');
dir.append(subdirName());
return dir;
}
// ----------------------------------------------------------
/**
* Get the time zone name associated with this authentication
* domain.
* @return the time zone name
*/
public String timeZoneName()
{
String result = super.timeZoneName();
if (result == null || result.equals(""))
{
result = NSTimeZone.getDefault().getID();
}
return result;
}
// ----------------------------------------------------------
/**
* An instance method wrapper for {@link #availableTimeZones()} to
* provide KVC access to that static method.
* @return an NSArray of strings representing the date formats available
*/
public NSArray<TimeZoneDescriptor> timeZones()
{
return availableTimeZones();
}
// ----------------------------------------------------------
/**
* Get the time format pattern associated with this authentication
* domain.
* @return the time format pattern
*/
public String timeFormat()
{
String result = super.timeFormat();
if (result == null || result.equals(""))
{
result = globalDefaultTimeFormat();
}
return result;
}
// ----------------------------------------------------------
/**
* An instance method wrapper for {@link #availableTimeFormats()} to
* provide KVC access to that static method.
* @return an NSArray of strings representing the time formats available
*/
public NSArray<String> timeFormats()
{
return availableTimeFormats();
}
// ----------------------------------------------------------
/**
* Get the date format pattern associated with this authentication
* domain.
* @return the date format pattern
*/
public String dateFormat()
{
String result = super.dateFormat();
if (result == null || result.equals(""))
{
result = globalDefaultDateFormat();
}
return result;
}
// ----------------------------------------------------------
/**
* An instance method wrapper for {@link #availableDateFormats()} to
* provide KVC access to that static method.
* @return an NSArray of strings representing the date formats available
*/
public NSArray<String> dateFormats()
{
return availableDateFormats();
}
//~ Public Static Methods .................................................
// ----------------------------------------------------------
/**
* Look up and return an authentication domain object by its
* (partial) property name. If you use a property called
* <code>authenticator.<i>MyAuthenticator</i></code> to define
* an authentication domain in a property file, then you can retrieve
* this authenticator using the name "<i>MyAuthenticator</i>".
*
* @param name the property name of the authenticator
* @return The matching AuthenticationDomain object
*/
public static AuthenticationDomain authDomainByName(String name)
{
ensureAuthDomainsLoaded();
return firstObjectMatchingQualifier(
EOSharedEditingContext.defaultSharedEditingContext(),
propertyName.eq("authenticator." + name),
null);
}
// ----------------------------------------------------------
/**
* Look up and return an authentication domain object by its
* (partial) property name. If you use a property called
* <code>authenticator.<i>MyAuthenticator</i></code> to define
* an authentication domain in a property file, then you can retrieve
* this authenticator using the name "<i>MyAuthenticator</i>".
*
* @param name the property name of the authenticator
* @return The matching AuthenticationDomain object
*/
public static AuthenticationDomain authDomainBySubdirName(String name)
{
ensureAuthDomainsLoaded();
if (authDomainsBySubdirName == null)
{
authDomainsBySubdirName =
new HashMap<String, AuthenticationDomain>();
for (AuthenticationDomain domain : authDomains)
{
authDomainsBySubdirName.put(domain.subdirName(), domain);
}
}
return authDomainsBySubdirName.get(name);
}
// ----------------------------------------------------------
/**
* Get a list of shared authentication domain objects that have
* already been loaded into the shared editing context.
* @return an array of all AuthenticationDomain objects
*/
public static NSArray<AuthenticationDomain> authDomains()
{
ensureAuthDomainsLoaded();
return authDomains;
}
// ----------------------------------------------------------
/**
* Get a list of shared authentication domain objects that have
* already been loaded into the shared editing context.
* @return an array of all AuthenticationDomain objects
*/
public static AuthenticationDomain defaultDomain()
{
if (defaultDomain == null)
{
ensureAuthDomainsLoaded();
// Set up a default domain, if possible
String defaultDomainName = Application.configurationProperties()
.getProperty("authenticator.default");
if (defaultDomainName != null)
{
try
{
log.debug("looking up default domain");
defaultDomain = authDomainByName(defaultDomainName);
}
catch (EOObjectNotAvailableException e)
{
log.error("Default authentication domain ("
+ defaultDomainName + ") does not exist.");
}
catch (EOUtilities.MoreThanOneException e)
{
log.error("Multiple entries for default authentication "
+ "domain (" + defaultDomainName + ")");
}
}
if (defaultDomain == null)
{
defaultDomain = authDomains().objectAtIndex(0);
}
}
return defaultDomain;
}
// ----------------------------------------------------------
/**
* Load the list of shared authentication domain objects, if it hasn't
* been done already.
*/
public static void ensureAuthDomainsLoaded()
{
if (authDomains == null)
{
refreshAuthDomains();
}
}
// ----------------------------------------------------------
/**
* Force reloading of the list of shared authentication domain objects.
*/
public static void refreshAuthDomains()
{
log.debug("refreshAuthDomains()");
theAuthenticatorMap = new TreeMap<String, UserAuthenticator>();
defaultDomain = null;
final WCProperties properties = Application.configurationProperties();
@SuppressWarnings("unchecked")
final Enumeration<String> propertyNames =
(Enumeration<String>)properties.propertyNames();
final String prefix = "authenticator.";
run(new ECAction() { public void action() {
log.debug("searching property list");
// Disconnect from the shared editing context, since we're actually
// making changes to objects in the shared context here.
ec.setSharedEditingContext(null);
while (propertyNames.hasMoreElements())
{
AuthenticationDomain domain = null;
String base = propertyNames.nextElement();
// log.debug ( "checking property: " + base );
if ( base.startsWith(prefix)
&& !base.equals("authenticator.default")
&& base.substring(prefix.length()).indexOf('.') == -1)
{
log.debug ("trying to register: " + base);
UserAuthenticator ua = null;
String uaClassName = properties.getProperty(base);
if (uaClassName == null || uaClassName.equals(""))
{
uaClassName = properties.getProperty(
"authenticator.default.class",
DatabaseAuthenticator.class.getName());
}
try
{
ua = (UserAuthenticator)
Class.forName(uaClassName).newInstance();
log.info(
"registering " + base + " with " + uaClassName);
ua.configure(base, properties);
theAuthenticatorMap.put(base, ua);
try
{
int oldDebugLevel =
NSLog.debug.allowedDebugLevel();
NSLog.debug.setAllowedDebugLevel(
NSLog.DebugLevelInformational);
domain = uniqueObjectMatchingQualifier(ec,
propertyName.eq(base));
NSLog.debug.setAllowedDebugLevel(oldDebugLevel);
if (domain != null)
{
log.debug("domain is already in database");
if (domain.propertyName() != base)
{
domain.setPropertyName(base);
}
String thisDisplayableName =
properties.getProperty(
base + "." + DISPLAYABLE_NAME_KEY);
if (thisDisplayableName != null
&& domain.displayableName()
!= thisDisplayableName)
{
domain.setDisplayableName(
thisDisplayableName);
}
String emailDomain =
properties.getProperty(
base + "." + DEFAULT_EMAIL_DOMAIN_KEY);
if (emailDomain != null
&& domain.defaultEmailDomain() !=
emailDomain)
{
domain.setDefaultEmailDomain(emailDomain);
}
}
else
{
log.info("Adding to AuthenticationDomain "
+ "table: " + base);
domain = new AuthenticationDomain();
ec.insertObject(domain);
domain.setPropertyName(base);
domain.setDisplayableName(
properties.getProperty(
base + "." + DISPLAYABLE_NAME_KEY));
domain.setDefaultEmailDomain(
properties.getProperty(base
+ "." + DEFAULT_EMAIL_DOMAIN_KEY));
}
}
catch (EOUtilities.MoreThanOneException e)
{
log.error("Error updating AuthenticationDomain "
+ "table for " + base, e);
}
if (domain != null)
{
String emailDomain = properties.getProperty(
base + "." + DEFAULT_EMAIL_DOMAIN_KEY);
if (emailDomain != null)
{
domain.setDefaultEmailDomain(emailDomain);
}
}
}
catch (Exception e)
{
log.error("refreshAuthDomains(): cannot create "
+ uaClassName + ":", e);
}
}
}
ec.saveChanges();
}});
log.debug("refreshing shared authentication domain objects");
authDomains = allObjectsOrderedByDisplayableName(
EOSharedEditingContext.defaultSharedEditingContext());
// TODO: can't do this yet, since the domain's authenticator class
// and config settings are not stored in the database!
// We'll need to change that, eventually.
// confirm that all domains are registered in the map
// for (int i = 0; i < authDomains.count(); i++)
// {
// AuthenticationDomain domain = authDomains.objectAtIndex( i );
// String name = domain.propertyName();
// if (theAuthenticatorMap.get(name) == null)
// {
//
// }
// }
}
// ----------------------------------------------------------
/**
* Get the list of available time formats for users to choose from.
* This value is loaded/parsed from the <code>timeFormats</code>
* configuration property, set under the Core subsystem's configuration
* settings. The list should be a list of strings containing patterns
* acceptable by {@link NSTimestampFormatter}.
* @return an NSArray of strings representing the time formats available
*/
public static NSArray<String> availableTimeFormats()
{
if (timeFormats == null)
{
try
{
@SuppressWarnings("unchecked")
NSArray<String> propValues = (NSArray<String>)Application
.configurationProperties().arrayForKey("timeFormats");
timeFormats = propValues;
}
catch (Exception e)
{
log.error("Exception parsing \"timeFormats\" property "
+ "setting as an NSArray:", e);
}
if (timeFormats == null)
{
timeFormats =
new NSArray<String>(new String[]{"%I:%M%p", "%H:%M"});
}
}
return timeFormats;
}
// ----------------------------------------------------------
/**
* Get the default time format pattern for users to choose from.
* The default is the first entry in the {@link #availableTimeFormats()}
* list.
* @return the default time format pattern
*/
public static String globalDefaultTimeFormat()
{
return availableTimeFormats().objectAtIndex(0);
}
// ----------------------------------------------------------
/**
* Get the list of available date formats for users to choose from.
* This value is loaded/parsed from the <code>dateFormats</code>
* configuration property, set under the Core subsystem's configuration
* settings. The list should be a list of strings containing patterns
* acceptable by {@link NSTimestampFormatter}.
* @return an NSArray of strings representing the date formats available
*/
public static NSArray<String> availableDateFormats()
{
if (dateFormats == null)
{
try
{
@SuppressWarnings("unchecked")
NSArray<String> propValues = (NSArray<String>)Application
.configurationProperties().arrayForKey( "dateFormats" );
dateFormats = propValues;
}
catch (Exception e)
{
log.error("Exception parsing \"dateFormats\" property "
+ "setting as an NSArray:", e);
}
if (dateFormats == null)
{
dateFormats = new NSArray<String>(new String[]{
"%m/%d/%y", "%m/%d/%Y", "%d.%m.%Y", "%d.%m.%y",
"%y-%m-%d", "%Y-%m-%d", "%d-%b-%y", "%d-%b-%Y" });
}
}
return dateFormats;
}
// ----------------------------------------------------------
/**
* Get the default date format pattern for users to choose from.
* The default is the first entry in the {@link #availableDateFormats()}
* list.
* @return the default date format pattern
*/
public static String globalDefaultDateFormat()
{
return availableDateFormats().objectAtIndex(0);
}
// ----------------------------------------------------------
/**
* A simple class that combines a time zone ID with a printable
* time zone name.
*/
public static class TimeZoneDescriptor
{
public String id;
public String printableName;
public TimeZoneDescriptor(String id)
{
this.id = id;
printableName = id.replaceAll("_", " ").replaceAll("/", ": ");
}
public NSTimeZone timeZone()
{
return NSTimeZone.timeZoneWithName(id, true);
}
public boolean equals(Object obj)
{
boolean result = false;
if (obj == null)
{
// result = false
}
else if (obj == this)
{
result = true;
}
else if (obj instanceof TimeZoneDescriptor)
{
TimeZoneDescriptor rhs = (TimeZoneDescriptor)obj;
result = (id == null)
? (rhs.id == null)
: (id.equals(rhs.id));
result = result && ((printableName == null)
? (rhs.printableName == null)
: (printableName.equals(rhs.printableName)));
}
return result;
}
}
// ----------------------------------------------------------
/**
* Get the list of available date formats for users to choose from.
* This value is loaded/parsed from the <code>dateFormats</code>
* configuration property, set under the Core subsystem's configuration
* settings. The list should be a list of strings containing patterns
* acceptable by {@link NSTimestampFormatter}.
* @return an NSArray of strings representing the date formats available
*/
public static NSArray<TimeZoneDescriptor> availableTimeZones()
{
if (timeZones == null)
{
NSMutableArray<TimeZoneDescriptor> zones =
new NSMutableArray<TimeZoneDescriptor>();
for (Object name : NSTimeZone.knownTimeZoneNames())
{
zones.add(new TimeZoneDescriptor(name.toString()));
}
ERXArrayUtilities.sortArrayWithKey(zones, "printableName");
timeZones = zones;
}
return timeZones;
}
// ----------------------------------------------------------
/**
* Get the {@link TimeZoneDescriptor} associated with the specified
* time zone name (id).
* @param id the time zone name (id) to look for
* @return The matching descriptor from the {@link #availableTimeZones()}
* list
*/
public static TimeZoneDescriptor descriptorForZone(String id)
{
for (TimeZoneDescriptor tzd : availableTimeZones())
{
if (tzd.id.equals(id))
{
return tzd;
}
}
return null;
}
// ----------------------------------------------------------
public static String subdirNameOf(String name)
{
String result = null;
if (name != null)
{
char[] chars = new char[name.length()];
int pos = 0;
for (int i = 0; i < name.length(); i++)
{
char c = name.charAt(i);
if (Character.isLetterOrDigit(c) ||
c == '_' ||
c == '-')
{
chars[pos] = c;
pos++;
}
}
result = new String(chars, 0, pos);
}
return result;
}
//~ Instance/static variables .............................................
private static NSArray<AuthenticationDomain> authDomains;
private static AuthenticationDomain defaultDomain;
private static Map<String, UserAuthenticator> theAuthenticatorMap;
private static Map<String, AuthenticationDomain> authDomainsBySubdirName;
private String cachedSubdirName = null;
private String cachedName = null;
private static NSArray<String> timeFormats;
private static NSArray<String> dateFormats;
private static NSArray<TimeZoneDescriptor> timeZones;
static Logger log = Logger.getLogger(AuthenticationDomain.class);
}