Extracted ooo-user bits into separate library.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" standalone="yes"?>
|
||||
<project name="ooo-user" default="compile" basedir=".">
|
||||
<property name="src.dir" location="src/main/java"/>
|
||||
<property name="deploy.dir" location="target"/>
|
||||
<property name="classes.dir" location="${deploy.dir}/classes"/>
|
||||
<property name="jar.file" location="${deploy.dir}/${ant.project.name}.jar"/>
|
||||
|
||||
<!-- bring in our standard build support -->
|
||||
<property name="ooo-build.vers" value="2.7"/>
|
||||
<ant antfile="etc/bootstrap.xml"/>
|
||||
<import file="${user.home}/.m2/ooo-build/${ooo-build.vers}/ooo-build.xml"/>
|
||||
|
||||
<target name="prepare" depends="-init-ooo">
|
||||
<mkdir dir="${classes.dir}"/>
|
||||
<mavendep pom="pom.xml"/>
|
||||
</target>
|
||||
|
||||
<target name="clean" description="Cleans out compilation results.">
|
||||
<delete dir="${deploy.dir}"/>
|
||||
</target>
|
||||
|
||||
<target name="compile" depends="prepare" description="Builds the Java code.">
|
||||
<ooojavac srcdir="${src.dir}" destdir="${classes.dir}" version="1.5"
|
||||
classpathref="pom.xml.path"/>
|
||||
</target>
|
||||
|
||||
<target name="package" depends="compile" description="Builds and jars the code.">
|
||||
<jar destfile="${jar.file}" basedir="${classes.dir}"/>
|
||||
</target>
|
||||
|
||||
<target name="maven-deploy" depends="package"
|
||||
description="Deploys our build artifacts to a Maven repository.">
|
||||
<mavendeploy file="${jar.file}" pom="pom.xml" srcdir="${src.dir}"/>
|
||||
</target>
|
||||
</project>
|
||||
@@ -0,0 +1,36 @@
|
||||
<project name="bootstrap" default="-extract-ooo-build">
|
||||
<!--
|
||||
Pulls in ooo-build from our maven repository to make our base build system available.
|
||||
From http://ooo-build.googlecode.com/hg/etc/bootstrap.xml
|
||||
|
||||
To use, copy this file into your project's directory and add
|
||||
|
||||
<property name="ooo-build.vers" value="2.1"/>
|
||||
<ant antfile="bootstrap.xml"/>
|
||||
<import file="${user.home}/.m2/ooo-build/${ooo-build.vers}/ooo-build.xml"/>
|
||||
|
||||
to the top level of your build.xml. Then you can depend on -init-ooo in your lowest level
|
||||
target to expose ooo-build's maven macros, javac macros, and antcontrib.
|
||||
-->
|
||||
|
||||
<property name="maven.dir" value="${user.home}/.m2"/>
|
||||
<property name="ooo-build.path"
|
||||
value="com/threerings/ooo-build/${ooo-build.vers}"/>
|
||||
<property name="ooo-build.jar"
|
||||
value="ooo-build-${ooo-build.vers}.jar"/>
|
||||
<property name="ooo-build.local.dir" value="${maven.dir}/repository/${ooo-build.path}"/>
|
||||
<property name="ooo-build.local.file" value="${ooo-build.local.dir}/${ooo-build.jar}"/>
|
||||
<condition property="ooo-build.exists"><available file="${ooo-build.local.file}"/></condition>
|
||||
<target name="-download-ooo-build" unless="ooo-build.exists">
|
||||
<mkdir dir="${ooo-build.local.dir}"/>
|
||||
<get src="http://ooo-maven.googlecode.com/hg/repository/${ooo-build.path}/${ooo-build.jar}"
|
||||
dest="${ooo-build.local.file}" usetimestamp="true"/>
|
||||
</target>
|
||||
|
||||
<property name="ooo-build.dir" value="${maven.dir}/ooo-build/${ooo-build.vers}"/>
|
||||
<condition property="extracted.exists"><available file="${ooo-build.dir}/ooo-build.xml"/></condition>
|
||||
<target name="-extract-ooo-build" depends="-download-ooo-build" unless="extracted.exists">
|
||||
<mkdir dir="${ooo-build.dir}"/>
|
||||
<unjar src="${ooo-build.local.file}" dest="${ooo-build.dir}"/>
|
||||
</target>
|
||||
</project>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.threerings</groupId>
|
||||
<artifactId>ooo-parent</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ooo-user</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>ooo-user</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.samskivert</groupId>
|
||||
<artifactId>depot</artifactId>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.samskivert</groupId>
|
||||
<artifactId>samskivert</artifactId>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>10.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>2.5</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<configuration>
|
||||
<links>
|
||||
<link>http://samskivert.googlecode.com/svn/apidocs/</link>
|
||||
<link>http://docs.guava-libraries.googlecode.com/git/javadoc/</link>
|
||||
</links>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,283 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.servlet;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.servlet.Site;
|
||||
import com.samskivert.servlet.SiteIdentifier;
|
||||
import com.samskivert.util.ArrayUtil;
|
||||
|
||||
import com.threerings.servlet.persist.DomainRecord;
|
||||
import com.threerings.servlet.persist.SiteIdentifierRepository;
|
||||
import com.threerings.servlet.persist.SiteRecord;
|
||||
|
||||
/**
|
||||
* Accomplishes the process of site identification based on a mapping from domains (e.g.
|
||||
* samskivert.com) to site identifiers that is maintained in a depot database table.
|
||||
*
|
||||
* <p> There are two tables, one that maps domains to site identifiers and another that maps site
|
||||
* identifiers to site strings. These are both loaded at construct time and refreshed periodically
|
||||
* in the course of normal operation.
|
||||
*
|
||||
* <p> Note that any of the calls to identify, lookup or enumerate site information can result in
|
||||
* the sites table being refreshed from the database which will take relatively much longer than
|
||||
* the simple hashtable lookup that the operations normally require. However, this happens only
|
||||
* once every 15 minutes and the circumstances in which the site identifier are normally used can
|
||||
* generally accomodate the extra 100 milliseconds or so that it is likely to take to reload the
|
||||
* (tiny) sites and domains tables from the database.
|
||||
*/
|
||||
public class DepotSiteIdentifier
|
||||
implements SiteIdentifier
|
||||
{
|
||||
/**
|
||||
* Constructs a debot site identifier.
|
||||
*/
|
||||
public DepotSiteIdentifier (PersistenceContext pctx)
|
||||
{
|
||||
this(pctx, DEFAULT_SITE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identifier that will load data from the supplied connection provider and which
|
||||
* will use the supplied default site id instead of {@link #DEFAULT_SITE_ID}.
|
||||
*/
|
||||
public DepotSiteIdentifier (PersistenceContext pctx, int defaultSiteId)
|
||||
{
|
||||
this(pctx, defaultSiteId, DEFAULT_SITE_STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identifier that will load data from the supplied connection provider and which
|
||||
* will use the supplied default site id instead of {@link #DEFAULT_SITE_ID} and the supplied
|
||||
* default site string instead of {@link #DEFAULT_SITE_STRING}.
|
||||
*/
|
||||
public DepotSiteIdentifier (
|
||||
PersistenceContext pctx, int defaultSiteId, String defaultSiteString)
|
||||
{
|
||||
_repo = new SiteIdentifierRepository(pctx);
|
||||
refreshSiteData();
|
||||
_defaultSiteId = defaultSiteId;
|
||||
_defaultSiteString = defaultSiteString;
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
public int identifySite (HttpServletRequest req)
|
||||
{
|
||||
checkReloadSites();
|
||||
String serverName = req.getServerName();
|
||||
|
||||
// scan for the mapping that matches the specified domain
|
||||
int msize = _mappings.size();
|
||||
for (int i = 0; i < msize; i++) {
|
||||
SiteMapping mapping = _mappings.get(i);
|
||||
if (serverName.endsWith(mapping.domain)) {
|
||||
return mapping.siteId;
|
||||
}
|
||||
}
|
||||
|
||||
// if we matched nothing, return the default id
|
||||
return _defaultSiteId;
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
public String getSiteString (int siteId)
|
||||
{
|
||||
checkReloadSites();
|
||||
Site site = _sitesById.get(siteId);
|
||||
if (site == null) {
|
||||
site = _sitesById.get(_defaultSiteId);
|
||||
}
|
||||
return (site == null) ? _defaultSiteString : site.siteString;
|
||||
}
|
||||
|
||||
// documentation inherited
|
||||
public int getSiteId (String siteString)
|
||||
{
|
||||
checkReloadSites();
|
||||
Site site = _sitesByString.get(siteString);
|
||||
return (site == null) ? _defaultSiteId : site.siteId;
|
||||
}
|
||||
|
||||
// documentation inherited from interface
|
||||
public Iterator<Site> enumerateSites ()
|
||||
{
|
||||
checkReloadSites();
|
||||
return _sitesById.values().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new site into the site table and into this mapping.
|
||||
*/
|
||||
public Site insertNewSite (String siteString)
|
||||
{
|
||||
return insertNewSite(siteString, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new site into the site table and into this mapping.
|
||||
*/
|
||||
public Site insertNewSite (String siteString, int siteId)
|
||||
{
|
||||
if (_sitesByString.containsKey(siteString) ||
|
||||
(siteId > 0 && _sitesById.containsKey(siteId))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// add it to the db
|
||||
Site site = _repo.insertNewSite(siteString, siteId);
|
||||
|
||||
// add it to our two mapping tables, taking care to avoid causing enumerateSites() to choke
|
||||
Map<String, Site> newStrings = Maps.newHashMap(_sitesByString);
|
||||
Map<Integer, Site> newIds = Maps.newHashMap(_sitesById);
|
||||
newIds.put(site.siteId, site);
|
||||
newStrings.put(site.siteString, site);
|
||||
_sitesByString = newStrings;
|
||||
_sitesById = newIds;
|
||||
|
||||
return site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new domain into the domain table and into this mapping.
|
||||
*/
|
||||
public void insertNewDomain (String domain, int siteId)
|
||||
{
|
||||
// scan for the mapping that matches the specified domain
|
||||
int msize = _mappings.size();
|
||||
for (int i = 0; i < msize; i++) {
|
||||
SiteMapping mapping = _mappings.get(i);
|
||||
if (mapping.domain.equals(domain)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// add it to the db
|
||||
_repo.insertNewDomain(domain, siteId);
|
||||
|
||||
// add it to our two mapping table, taking care to avoid causing enumerateSites() to choke
|
||||
List<SiteMapping> mappings = Lists.newArrayList();
|
||||
mappings.addAll(_mappings);
|
||||
mappings.add(new SiteMapping(siteId, domain));
|
||||
|
||||
Collections.sort(mappings, SiteMapping.BY_SPECIFICITY);
|
||||
_mappings = mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if we should reload our sites information from the sites table.
|
||||
*/
|
||||
protected void checkReloadSites ()
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
boolean reload = false;
|
||||
synchronized (this) {
|
||||
reload = (now - _lastReload > RELOAD_INTERVAL);
|
||||
if (reload) {
|
||||
_lastReload = now;
|
||||
}
|
||||
}
|
||||
if (reload) {
|
||||
refreshSiteData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the cached site information.
|
||||
*/
|
||||
public void refreshSiteData ()
|
||||
{
|
||||
List<SiteRecord> siteRecords = _repo.loadSites();
|
||||
Map<Integer,Site> sites = Maps.newHashMap();
|
||||
Map<String,Site> strings = Maps.newHashMap();
|
||||
for (SiteRecord record : siteRecords) {
|
||||
Site site = record.toSite();
|
||||
sites.put(record.siteId, site);
|
||||
strings.put(record.siteString, site);
|
||||
}
|
||||
_sitesById = sites;
|
||||
_sitesByString = strings;
|
||||
|
||||
List<DomainRecord> domainRecords = _repo.loadDomains();
|
||||
List<SiteMapping> mappings = Lists.newArrayList();
|
||||
for (DomainRecord record : domainRecords) {
|
||||
mappings.add(new SiteMapping(record.siteId, record.domain));
|
||||
}
|
||||
|
||||
Collections.sort(mappings, SiteMapping.BY_SPECIFICITY);
|
||||
_mappings = mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to track domain to site identifier mappings.
|
||||
*/
|
||||
protected static class SiteMapping
|
||||
{
|
||||
/**
|
||||
* Sorts site mappings from most specific (www.yahoo.com) to least specific (yahoo.com).
|
||||
*/
|
||||
public static final Comparator<SiteMapping> BY_SPECIFICITY = new Comparator<SiteMapping>() {
|
||||
public int compare (SiteMapping one, SiteMapping two) {
|
||||
return one._rdomain.compareTo(two._rdomain);
|
||||
}
|
||||
};
|
||||
|
||||
/** The domain to match. */
|
||||
public String domain;
|
||||
|
||||
/** The site identifier for the associated domain. */
|
||||
public int siteId;
|
||||
|
||||
public SiteMapping (int siteId, String domain) {
|
||||
this.siteId = siteId;
|
||||
this.domain = domain;
|
||||
byte[] bytes = domain.getBytes();
|
||||
ArrayUtil.reverse(bytes);
|
||||
_rdomain = new String(bytes);
|
||||
}
|
||||
|
||||
@Override // from Object
|
||||
public String toString () {
|
||||
return "[" + domain + " => " + siteId + "]";
|
||||
}
|
||||
|
||||
protected String _rdomain;
|
||||
}
|
||||
|
||||
/** The repository through which we load up site identifier information. */
|
||||
protected SiteIdentifierRepository _repo;
|
||||
|
||||
/** The site id to return if we cannot identify the site from our table data. */
|
||||
protected int _defaultSiteId;
|
||||
|
||||
/** The site string to return if we cannot identify the site from our table data. */
|
||||
protected String _defaultSiteString;
|
||||
|
||||
/** The list of domain to site identifier mappings ordered from most specific domain to least
|
||||
* specific. */
|
||||
protected volatile List<SiteMapping> _mappings = Lists.newArrayList();
|
||||
|
||||
/** The mapping from integer site identifiers to string site identifiers. */
|
||||
protected volatile Map<Integer, Site> _sitesById = Maps.newHashMap();
|
||||
|
||||
/** The mapping from string site identifiers to integer site identifiers. */
|
||||
protected volatile Map<String, Site> _sitesByString = Maps.newHashMap();
|
||||
|
||||
/** Used to periodically reload our site data. */
|
||||
protected long _lastReload;
|
||||
|
||||
/** Reload our site data every 15 minutes. */
|
||||
protected static final long RELOAD_INTERVAL = 15 * 60 * 1000L;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.servlet.persist;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.servlet.SiteIdentifier;
|
||||
|
||||
/**
|
||||
* Represents a domain mapping known to a {@link SiteIdentifier}.
|
||||
*/
|
||||
@Entity(name="domains")
|
||||
public class DomainRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<DomainRecord> _R = DomainRecord.class;
|
||||
public static final ColumnExp<String> DOMAIN = colexp(_R, "domain");
|
||||
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The domain. */
|
||||
@Id @Column(length=128)
|
||||
public String domain;
|
||||
|
||||
/** The site identifier. */
|
||||
public int siteId;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link DomainRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<DomainRecord> getKey (String domain)
|
||||
{
|
||||
return newKey(_R, domain);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(DOMAIN); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.servlet.persist;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.samskivert.depot.DepotRepository;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.servlet.Site;
|
||||
|
||||
/**
|
||||
* Depot implements of a site identifier repository.
|
||||
*/
|
||||
@Singleton
|
||||
public class SiteIdentifierRepository extends DepotRepository
|
||||
{
|
||||
/**
|
||||
* Creates the repository.
|
||||
*/
|
||||
@Inject public SiteIdentifierRepository (PersistenceContext ctx)
|
||||
{
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the sites.
|
||||
*/
|
||||
public List<SiteRecord> loadSites ()
|
||||
{
|
||||
return findAll(SiteRecord.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the domains.
|
||||
*/
|
||||
public List<DomainRecord> loadDomains ()
|
||||
{
|
||||
return findAll(DomainRecord.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new site.
|
||||
*/
|
||||
public Site insertNewSite (String siteString)
|
||||
{
|
||||
return insertNewSite(siteString, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new site.
|
||||
*/
|
||||
public Site insertNewSite (String siteString, int siteId)
|
||||
{
|
||||
SiteRecord record = new SiteRecord();
|
||||
record.siteString = siteString;
|
||||
record.siteId = siteId;
|
||||
insert(record);
|
||||
return record.toSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new domain.
|
||||
*/
|
||||
public void insertNewDomain (String domain, int siteId)
|
||||
{
|
||||
DomainRecord record = new DomainRecord();
|
||||
record.domain = domain;
|
||||
record.siteId = siteId;
|
||||
insert(record);
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
|
||||
{
|
||||
classes.add(DomainRecord.class);
|
||||
classes.add(SiteRecord.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.servlet.persist;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.GenerationType;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
|
||||
import com.samskivert.servlet.Site;
|
||||
import com.samskivert.servlet.SiteIdentifier;
|
||||
|
||||
/**
|
||||
* Represents a site mapping known to a {@link SiteIdentifier}.
|
||||
*/
|
||||
@Entity(name="sites")
|
||||
public class SiteRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<SiteRecord> _R = SiteRecord.class;
|
||||
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
|
||||
public static final ColumnExp<String> SITE_STRING = colexp(_R, "siteString");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The site's unique identifier. */
|
||||
@Id @GeneratedValue(strategy=GenerationType.AUTO)
|
||||
public int siteId;
|
||||
|
||||
/** The site's human readable identifier (i.e., "monkeybutter"). */
|
||||
@Column(length=24)
|
||||
public String siteString;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link SiteRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<SiteRecord> getKey (int siteId)
|
||||
{
|
||||
return newKey(_R, siteId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(SITE_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
|
||||
/**
|
||||
* Creates a Site from this record.
|
||||
*/
|
||||
public Site toSite ()
|
||||
{
|
||||
Site site = new Site();
|
||||
site.siteId = siteId;
|
||||
site.siteString = siteString;
|
||||
return site;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.sql.PreparedStatement;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.SimpleRepository;
|
||||
import com.samskivert.util.IntTuple;
|
||||
|
||||
/**
|
||||
* Manages database access for figuring out if we're doing an AB test of the website, and if so,
|
||||
* keeping track of how many have gone each way.
|
||||
*/
|
||||
public class ABTestRepository extends SimpleRepository
|
||||
{
|
||||
/**
|
||||
* The database identifier used when establishing a database
|
||||
* connection. This value being <code>abtestdb</code>.
|
||||
*/
|
||||
public static final String ABTEST_DB_IDENT = "abtestdb";
|
||||
|
||||
/**
|
||||
* Creates the repository and prepares it for operation.
|
||||
*
|
||||
* @param provider the database connection provider.
|
||||
*/
|
||||
public ABTestRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, ABTEST_DB_IDENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up whether or not we're doing an AB test, and what the remaining desired number
|
||||
* of people for each bucket is.
|
||||
*
|
||||
* Returns null if there's no test running.
|
||||
*/
|
||||
public IntTuple getABTestCounts ()
|
||||
throws PersistenceException
|
||||
{
|
||||
return execute(new Operation<IntTuple>() {
|
||||
public IntTuple invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
Statement stmt = conn.createStatement();
|
||||
try {
|
||||
ResultSet rs =
|
||||
stmt.executeQuery("select REMAINING_A, REMAINING_B from AB_TEST");
|
||||
while (rs.next()) {
|
||||
int a = rs.getInt(1);
|
||||
int b = rs.getInt(2);
|
||||
|
||||
if (a != 0 && b != 0) {
|
||||
return new IntTuple(a,b);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void decrementABTest (final boolean groupA)
|
||||
throws PersistenceException
|
||||
{
|
||||
final IntTuple counts = getABTestCounts();
|
||||
|
||||
if (counts == null) {
|
||||
// Not running a test? Ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
if (counts.left == -1) {
|
||||
// Can set to -1/-1 to show we want it to go on indefinitely
|
||||
return;
|
||||
}
|
||||
|
||||
executeUpdate(new Operation<Object>() {
|
||||
public Object invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"update AB_TEST set REMAINING_" + (groupA ? "A" : "B") +
|
||||
"=" + (Math.max(0, (groupA ? counts.left : counts.right) - 1)));
|
||||
|
||||
try {
|
||||
JDBCUtil.checkedUpdate(stmt, 1);
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
String[] abTestTable = {
|
||||
"REMAINING_A INTEGER",
|
||||
"REMAINING_B INTEGER",
|
||||
};
|
||||
JDBCUtil.createTableIfMissing(conn, "AB_TEST", abTestTable, "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Representation of a AccountAction record from the database.
|
||||
*/
|
||||
public class AccountAction
|
||||
{
|
||||
/** An action constant indicating the first time a user becomes a
|
||||
* subscriber, and have also never before bought coins or time. */
|
||||
public static final int INITIAL_SUBSCRIPTION = 0;
|
||||
|
||||
/** An action constant indicating that some server has modified the
|
||||
* coins count for a particular user. */
|
||||
public static final int COINS_UPDATED = 1;
|
||||
|
||||
/** An action constant indicating that the user, whom at some point in
|
||||
* the past used to be a subscriber, has become a subscriber again. */
|
||||
public static final int REPEAT_SUBSCRIPTION = 2;
|
||||
|
||||
/** An action constant indicating that the user purchased coins for the
|
||||
* first time ever, and have never before subscribed or bought time. */
|
||||
public static final int INITIAL_COIN_PURCHASE = 3;
|
||||
|
||||
/** An action constant indicating that the specified account has
|
||||
* expired and any player resources should be expired. The 'data'
|
||||
* field is currently filled in with the yohoho accountId for the
|
||||
* expired account. */
|
||||
public static final int ACCOUNT_EXPIRED = 4;
|
||||
|
||||
/** An action constant indicating that the specified account has been
|
||||
* deleted in the external account system. The 'data' field is
|
||||
* optionally filled in with the "disabled" username which the server
|
||||
* can use to simply disable the account rather than deleting it. */
|
||||
public static final int ACCOUNT_DELETED = 5;
|
||||
|
||||
/** An action unconnected with any single account, but rather a system-wide
|
||||
* signal to all servers that they should do whatever "daily" actions
|
||||
* they would like to do at the present time. */
|
||||
public static final int DO_DAILY_ACTIONS = 6;
|
||||
|
||||
/** An action constant indicating that the user purchased time for the
|
||||
* first time ever, and has never before subscribed or bought coins. */
|
||||
public static final int INITIAL_TIME_PURCHASE = 7;
|
||||
|
||||
/** Indicates that a reward has been activated for the account. */
|
||||
public static final int REWARD_ACTIVATED = 8;
|
||||
|
||||
/** Placeholder for application-defined action constants. */
|
||||
public static final int FIRST_APPLICATION_ACTION = 100;
|
||||
|
||||
/** Unique action identifier. */
|
||||
public int actionId;
|
||||
|
||||
/** The unique identifier for the account. */
|
||||
public String accountName;
|
||||
|
||||
/** The action that took place. */
|
||||
public int action;
|
||||
|
||||
/** Data that is interpreted depending on the action type. */
|
||||
public String data;
|
||||
|
||||
/** When the action took place. */
|
||||
public Timestamp entered;
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.JORARepository;
|
||||
import com.samskivert.jdbc.jora.Table;
|
||||
import com.samskivert.util.ArrayIntSet;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Provides access to the account actions table.
|
||||
*
|
||||
* @deprecated Use com.threerings.user.depot.AccountActionRepository instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class AccountActionRepository extends JORARepository
|
||||
{
|
||||
/**
|
||||
* The database identifier used when establishing a database connection.
|
||||
* This value being <code>actiondb</code>.
|
||||
*/
|
||||
public static final String ACTION_DB_IDENT = "actiondb";
|
||||
|
||||
/**
|
||||
* Creates the repository and prepares it for operation.
|
||||
*
|
||||
* @param provider the database connection provider.
|
||||
*/
|
||||
public AccountActionRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, ACTION_DB_IDENT);
|
||||
|
||||
// figure out whether or not we should be disabled
|
||||
execute(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
_active = JDBCUtil.tableExists(conn, "ACCOUNT_ACTIONS");
|
||||
if (!_active) {
|
||||
log.info("No actions table. Disabling.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of actions that have not yet been processed by the specified server.
|
||||
* Returns all actions if no server name is specified.
|
||||
*
|
||||
* <em>Note:</em> this method has two side effects: the first time it is called, it will
|
||||
* register this server as a action participant (assuming server is non-null) and
|
||||
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
|
||||
* hour.
|
||||
*/
|
||||
public List<AccountAction> getActions (String server)
|
||||
throws PersistenceException
|
||||
{
|
||||
return getActions(server, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of actions that have not yet been processed by the specified server.
|
||||
* Returns all actions if no server name is specified.
|
||||
*
|
||||
* @param maxActions the maximum number of actions to return.
|
||||
*
|
||||
* <em>Note:</em> this method has two side effects: the first time it is called, it will
|
||||
* register this server as a action participant (assuming server is non-null) and
|
||||
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
|
||||
* hour.
|
||||
*/
|
||||
public List<AccountAction> getActions (String server, int maxActions)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
return new ArrayList<AccountAction>();
|
||||
}
|
||||
|
||||
// look ma, subselects
|
||||
String where = "where ACTION_ID NOT IN " +
|
||||
"(select ACTION_ID from PROCESSED_ACTIONS " +
|
||||
"where SERVER = '" + server + "') limit " + maxActions;
|
||||
List<AccountAction> list = loadAll(_atable, where);
|
||||
|
||||
// unjigger the account names
|
||||
for (AccountAction action : list) {
|
||||
action.accountName = JDBCUtil.unjigger(action.accountName);
|
||||
}
|
||||
|
||||
// no fooling around if we are acting in general
|
||||
if (StringUtil.isBlank(server)) {
|
||||
return list;
|
||||
}
|
||||
|
||||
// if this is the first time this method is called, register ourselves
|
||||
// with the action system
|
||||
long now = System.currentTimeMillis();
|
||||
if (_lastActionPrune == 0L) {
|
||||
_lastActionPrune = now;
|
||||
try {
|
||||
registerActionServer(server);
|
||||
} catch (PersistenceException pe) {
|
||||
log.warning("Failure registering server", "server", server, pe);
|
||||
}
|
||||
|
||||
} else if (now - _lastActionPrune > ACTION_PRUNE_INTERVAL) {
|
||||
_lastActionPrune = now;
|
||||
try {
|
||||
pruneActions();
|
||||
} catch (PersistenceException pe) {
|
||||
log.warning("Failure auto-pruning actions", "server", server, pe);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository.
|
||||
*/
|
||||
public void addAction (String accountName, int action)
|
||||
throws PersistenceException
|
||||
{
|
||||
addAction(accountName, null, action, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository.
|
||||
*/
|
||||
public void addAction (String accountName, String data, int action)
|
||||
throws PersistenceException
|
||||
{
|
||||
addAction(accountName, data, action, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository.
|
||||
*/
|
||||
public void addAction (String accountName, int action, String server)
|
||||
throws PersistenceException
|
||||
{
|
||||
addAction(accountName, null, action, server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository, with the specified server being marked as already
|
||||
* having processed the action.
|
||||
*/
|
||||
public void addAction (String accountName, String data, int action, final String server)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
log.info("Dropping account action",
|
||||
"name", accountName, "data", data, "action", action, "server", server);
|
||||
return;
|
||||
}
|
||||
|
||||
// create and insert the account action
|
||||
final AccountAction aact = new AccountAction();
|
||||
aact.accountName = JDBCUtil.jigger(accountName);
|
||||
aact.data = data;
|
||||
aact.action = action;
|
||||
aact.entered = new Timestamp(System.currentTimeMillis());
|
||||
aact.actionId = insert(_atable, aact);
|
||||
|
||||
// note that it was processed by this server if appropriate
|
||||
if (!StringUtil.isBlank(server)) {
|
||||
noteProcessed(aact.actionId, server);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the single specified action to reflect that the specified server has processed it.
|
||||
*/
|
||||
public void updateAction (AccountAction action, String server)
|
||||
throws PersistenceException
|
||||
{
|
||||
updateActions(Collections.singletonList(action), server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update a list of actions.
|
||||
*/
|
||||
public void updateActions (List<AccountAction> actions, String server)
|
||||
throws PersistenceException
|
||||
{
|
||||
for (int ii = 0, ll = actions.size(); ii < ll; ii++) {
|
||||
noteProcessed(actions.get(ii).actionId, server);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an action from the repository.
|
||||
*/
|
||||
public void deleteAction (final AccountAction action)
|
||||
throws PersistenceException
|
||||
{
|
||||
// delete the action
|
||||
delete(_atable, action);
|
||||
|
||||
// and any records indicating it has been processed
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
Statement stmt = conn.createStatement();
|
||||
try {
|
||||
stmt.executeUpdate("delete from PROCESSED_ACTIONS " +
|
||||
"where ACTION_ID = " + action.actionId);
|
||||
} finally {
|
||||
stmt.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes that the specified server has processed the sepecified action.
|
||||
*/
|
||||
public void noteProcessed (final int actionId, final String server)
|
||||
throws PersistenceException
|
||||
{
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
Statement stmt = conn.createStatement();
|
||||
try {
|
||||
String query = "insert into PROCESSED_ACTIONS " +
|
||||
"values(" + actionId + ", '" + server + "')";
|
||||
stmt.executeUpdate(query);
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes actions that have been processed by all registered servers to keep the actions table
|
||||
* small. This method need not be called by hand as it is called automatically by
|
||||
* {@link #getActions(String,int)}, but no more frequently than once an hour.
|
||||
*/
|
||||
public void pruneActions ()
|
||||
throws PersistenceException
|
||||
{
|
||||
final Set<String> servers = loadActionServers();
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
Map<Integer,Set<String>> procmap = Maps.newHashMap();
|
||||
ArrayIntSet actids = new ArrayIntSet();
|
||||
Statement stmt = conn.createStatement();
|
||||
try {
|
||||
// determine which servers have processed which actions
|
||||
ResultSet rs = stmt.executeQuery(
|
||||
"select ACTION_ID, SERVER from PROCESSED_ACTIONS");
|
||||
while (rs.next()) {
|
||||
Integer actionId = (Integer)rs.getObject(1);
|
||||
Set<String> procset = procmap.get(actionId);
|
||||
if (procset == null) {
|
||||
procmap.put(actionId, procset = Sets.newHashSet());
|
||||
}
|
||||
procset.add(rs.getString(2));
|
||||
}
|
||||
|
||||
// determine which of those have been fully processed
|
||||
for (Map.Entry<Integer,Set<String>> entry : procmap.entrySet()) {
|
||||
Integer actionId = entry.getKey();
|
||||
Set<String> procset = entry.getValue();
|
||||
if (procset.containsAll(servers)) {
|
||||
actids.add(actionId.intValue());
|
||||
}
|
||||
}
|
||||
|
||||
// now wipe out the actions (and processed entries) for
|
||||
// all actions that have been fully processed
|
||||
if (actids.size() > 0) {
|
||||
String where = "where ACTION_ID in " +
|
||||
"(" + Joiner.on(",").join(actids) + ")";
|
||||
stmt.executeUpdate("delete from ACCOUNT_ACTIONS " + where);
|
||||
stmt.executeUpdate("delete from PROCESSED_ACTIONS " + where);
|
||||
}
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a server with the action system. This method is called automatically the first
|
||||
* time {@link #getActions(String)} is called.
|
||||
*/
|
||||
protected void registerActionServer (final String server)
|
||||
throws PersistenceException
|
||||
{
|
||||
// see if we're already registered
|
||||
Set<String> set = loadActionServers();
|
||||
if (set.contains(server)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if not, insert ourselves into the action table
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
String query = "insert into ACTION_SERVERS (SERVER) values (?)";
|
||||
PreparedStatement stmt = null;
|
||||
try {
|
||||
stmt = conn.prepareStatement(query);
|
||||
stmt.setString(1, server);
|
||||
stmt.executeUpdate();
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log.info("Registered action server '" + server + "'.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up the set of action servers.
|
||||
*/
|
||||
protected Set<String> loadActionServers ()
|
||||
throws PersistenceException
|
||||
{
|
||||
return execute(new Operation<Set<String>>() {
|
||||
public Set<String> invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
Set<String> set = Sets.newHashSet();
|
||||
String query = "select * from ACTION_SERVERS";
|
||||
Statement stmt = null;
|
||||
try {
|
||||
stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(query);
|
||||
while (rs.next()) {
|
||||
set.add(rs.getString(1));
|
||||
}
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTables ()
|
||||
{
|
||||
_atable = new Table<AccountAction>(AccountAction.class, TABLE, "ACTION_ID", true);
|
||||
}
|
||||
|
||||
/** The table used to note actions that have taken place. */
|
||||
protected Table<AccountAction> _atable;
|
||||
|
||||
/** Whether or not we're actually being used. */
|
||||
protected boolean _active;
|
||||
|
||||
/** Used by {@link #getActions(String)}. */
|
||||
protected long _lastActionPrune;
|
||||
|
||||
/** We automatically prune the actions table once an hour. */
|
||||
protected static final long ACTION_PRUNE_INTERVAL = 60 * 60 * 1000L;
|
||||
|
||||
/** The name of the account actions table. */
|
||||
protected static final String TABLE = "ACCOUNT_ACTIONS";
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.servlet.SiteIdentifier;
|
||||
import com.samskivert.servlet.util.CookieUtil;
|
||||
import com.samskivert.servlet.util.ParameterUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
public abstract class AffiliateInfo
|
||||
{
|
||||
/** The name of the session attribute used to hold the affiliate suffix */
|
||||
public static final String AFFILIATE_SUFFIX_ATTRIBUTE =
|
||||
AffiliateInfo.class.getName() + ".AffiliateSuffix";
|
||||
|
||||
/**
|
||||
* Class determining the behavior if the affiliate is 'new referral' i.e. the URL
|
||||
* has a 'from' and optionally, a 'tag' parameter.
|
||||
*/
|
||||
protected static class NewReferral extends AffiliateInfo
|
||||
{
|
||||
protected final String fromParameter;
|
||||
|
||||
protected final int siteId;
|
||||
|
||||
protected final int tagId;
|
||||
|
||||
public NewReferral (String fromParameter, int siteId, int tagId)
|
||||
{
|
||||
this.fromParameter = fromParameter;
|
||||
this.siteId = siteId;
|
||||
this.tagId = tagId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
if (isPersonalSuffix(fromParameter)) {
|
||||
setAffiliateSuffix(req, fromParameter, tagId);
|
||||
} else {
|
||||
setAffiliateSuffix(req, "" + siteId, tagId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return fromParameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return !isPersonalSuffix(fromParameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
if (isPersonalSuffix(fromParameter)) {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, fromParameter);
|
||||
} else {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// TODO probably need nothing
|
||||
giveCookie(req, rsp, SITE_REFER_COOKIE, fromParameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return tagId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class determining the behavior if the wasn't referred to us in this particular
|
||||
* request, but they were referred to us before, and we've recorded that in cookies.
|
||||
*/
|
||||
protected static class CurrentCookies extends AffiliateInfo
|
||||
{
|
||||
protected final String siteIdCookieValue;
|
||||
|
||||
protected final int tagIdCookieValue;
|
||||
|
||||
public CurrentCookies (String siteIdCookieValue, int tagIdCookieValue)
|
||||
{
|
||||
this.siteIdCookieValue = siteIdCookieValue;
|
||||
this.tagIdCookieValue = tagIdCookieValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
setAffiliateSuffix(req, siteIdCookieValue, tagIdCookieValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagIdCookieValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteIdCookieValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, SITE_REFER_COOKIE, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return tagIdCookieValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class determining the behavior if the user has an 'old-style' cookie indicating
|
||||
* that they came to us as a referral.
|
||||
*/
|
||||
protected static class LegacyCookies extends AffiliateInfo
|
||||
{
|
||||
protected final String siteRefer;
|
||||
|
||||
protected final int siteId;
|
||||
|
||||
public LegacyCookies (int siteId, String siteReferCookie)
|
||||
{
|
||||
this.siteRefer = siteReferCookie;
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
if (isPersonalSuffix(siteRefer)) {
|
||||
setAffiliateSuffix(req, siteRefer, AffiliateTag.NO_TAG);
|
||||
} else {
|
||||
setAffiliateSuffix(req, "" + siteId, AffiliateTag.NO_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
if (!isPersonalSuffix(siteRefer)) {
|
||||
return siteRefer;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return !isPersonalSuffix(siteRefer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// TODO: should we actually be removing the cookie here
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
if (isPersonalSuffix(siteRefer)) {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteRefer);
|
||||
} else {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// TODO: should be be removing this cookie?
|
||||
giveCookie(req, rsp, SITE_REFER_COOKIE, siteRefer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return AffiliateTag.NO_TAG;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the behavior if we don't have any affiliate information but we want to track the user
|
||||
* in a separate category from the 'default site'.
|
||||
*/
|
||||
protected static class AlternateDefault extends AffiliateInfo
|
||||
{
|
||||
protected final int site;
|
||||
|
||||
protected AlternateDefault (int site)
|
||||
{
|
||||
this.site = site;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDefaultAffiliate ()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
setAffiliateSuffix(req, "" + site, AffiliateTag.NO_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return AffiliateTag.NO_TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// Don't issue a cookie;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + site);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// Don't issue a cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the behavior if a tag cookie exists, but not an affiliate (site id) cookie.
|
||||
* "0" will be supplied as the affiliate to the 'affsuf' context attribute.
|
||||
*/
|
||||
protected static class TagOnly extends AffiliateInfo
|
||||
{
|
||||
protected final int tagId;
|
||||
|
||||
public TagOnly (int tagId)
|
||||
{
|
||||
this.tagId = tagId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
setAffiliateSuffix(req, "0", tagId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, String.valueOf(tagId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// do not issue a cookie
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// do not issue a cookie
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return tagId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out whether we know the name of the affiliate.
|
||||
*/
|
||||
public abstract boolean hasName ();
|
||||
|
||||
/**
|
||||
* Get the name of the affiliate.
|
||||
*
|
||||
* @return - The name as a string.
|
||||
*/
|
||||
public abstract String getAffiliateName ();
|
||||
|
||||
/**
|
||||
* Get the id for the affiliate tag.
|
||||
*/
|
||||
public abstract int getTagId ();
|
||||
|
||||
/**
|
||||
* Compute the appropriate affiliate 'affsuf' and add it to the session along with the
|
||||
* 'affid' used by some of the web templates. Now with less Velocity.
|
||||
*/
|
||||
public abstract void addToContext (HttpServletRequest req);
|
||||
|
||||
/**
|
||||
* Issue the site_id cookie to the browser (non-velocity).
|
||||
*/
|
||||
public abstract void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp);
|
||||
|
||||
/**
|
||||
* Issue the site_refer cookie to the browser.
|
||||
*/
|
||||
public abstract void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp);
|
||||
|
||||
/**
|
||||
* Issue the affiliate_tag cookie to the browser (non-velocity).
|
||||
*/
|
||||
public abstract void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp);
|
||||
|
||||
/**
|
||||
* Normally returns false, but can be overridden to indicate that we've been given
|
||||
* a cookie with a default affiliate passed in to us. "Default" isn't quite the right word
|
||||
* since this is really used for letting pages hardwire their default affiliate to be something
|
||||
* other than the default, but everything else uses this word, and I don't have a better idea
|
||||
* offhand.
|
||||
*/
|
||||
public boolean isDefaultAffiliate ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an affiliate info object from the cookies and parameters in the request.
|
||||
*/
|
||||
public static AffiliateInfo getAffiliateInfo (
|
||||
OOOUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
|
||||
HttpServletRequest req, boolean parseFromParameter) throws PersistenceException
|
||||
{
|
||||
int siteId = identifier.identifySite(req);
|
||||
return getAffiliateInfo(usermgr, repository, identifier, req, siteId, parseFromParameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an affiliate info object from the cookies and parameters in the request.
|
||||
*
|
||||
* @param alternateDefault - override default affiliate (hard-coded into affiliate pages)
|
||||
* @param parseFromParameter - Should we look at the request parameter "from"?
|
||||
* @return - The affiliate info or null if there wasn't enough information in the request to
|
||||
* construct it.
|
||||
* @throws PersistenceException - Thrown if accessing the ooouser database failed.
|
||||
*/
|
||||
public static AffiliateInfo getAffiliateInfo (
|
||||
OOOUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
|
||||
HttpServletRequest req, int alternateDefault,
|
||||
boolean parseFromParameter) throws PersistenceException
|
||||
{
|
||||
// if they're already a user with us, bail
|
||||
if (null != usermgr.loadUser(req)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String fromParameter = parseFromParameter ? parseFromParameter(req) : "";
|
||||
|
||||
// is this a new referral - i.e. this request was referred from a referral link?
|
||||
if (!StringUtil.isBlank(fromParameter)) {
|
||||
String tagParameter = readAffiliateTag(req);
|
||||
|
||||
int siteId = parseReferrer(repository, identifier, fromParameter);
|
||||
if (siteId == 0) {
|
||||
log.warning("User referred by bogus referrer [referrer=" + fromParameter + "].");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!StringUtil.isBlank(tagParameter) && !isPersonalSuffix(fromParameter)) {
|
||||
int tagId = usermgr.getAffiliateTagId(tagParameter);
|
||||
|
||||
return new NewReferral(fromParameter, siteId, tagId);
|
||||
} else {
|
||||
return new NewReferral(fromParameter, siteId, AffiliateTag.NO_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
// do we have modern cookies for this referral?
|
||||
String siteIdCookieValue = readSiteIdCookie(req);
|
||||
if (!StringUtil.isBlank(siteIdCookieValue)) {
|
||||
int tagIdCookieValue = readTagId(req);
|
||||
|
||||
return new CurrentCookies(siteIdCookieValue, tagIdCookieValue);
|
||||
}
|
||||
|
||||
// do we have old fashioned cookies for this referral?
|
||||
String siteReferCookie = CookieUtil.getCookieValue(req, SITE_REFER_COOKIE);
|
||||
if (!StringUtil.isBlank(siteReferCookie)) {
|
||||
int siteId = parseReferrer(repository, identifier, siteReferCookie);
|
||||
return new LegacyCookies(siteId, siteReferCookie);
|
||||
}
|
||||
|
||||
// we have no useful affiliate information. If we have been provided with an alternative
|
||||
// default site, then use that as the site-id.
|
||||
if (alternateDefault != identifier.identifySite(req)) {
|
||||
return new AlternateDefault(alternateDefault);
|
||||
}
|
||||
|
||||
// we have a tag cookie but no affiliate cookie
|
||||
int tagIdCookieValue = readTagId(req);
|
||||
if (tagIdCookieValue != AffiliateTag.NO_TAG) {
|
||||
return new TagOnly(tagIdCookieValue);
|
||||
}
|
||||
|
||||
// we couldn't figure out anything useful to do with whatever affiliate related info
|
||||
// we had, so we return null and the caller can do nothing.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param repository For verifying referrer ids against the database
|
||||
* @param identifier Velocity site repository
|
||||
* @param referrer Referral request string, may be a personal referrer of
|
||||
* the format r[0-9]+ or user-[0-9]+, or a site referrer of format [0-9]+
|
||||
* @return the referrer siteId, which may be a positive or negative number,
|
||||
* or 0 if no match.
|
||||
*/
|
||||
public static int parseReferrer (ReferralRepository repository, SiteIdentifier identifier,
|
||||
String referrer)
|
||||
{
|
||||
// blank referrer string
|
||||
if (StringUtil.isBlank(referrer)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// handle new-style personal referrals
|
||||
if (referrer.matches("r[0-9]+")) {
|
||||
int refId = 0;
|
||||
try {
|
||||
refId = Integer.parseInt(referrer.substring(1));
|
||||
} catch (NumberFormatException nfe) {
|
||||
log.warning("Bogus user referral: " + referrer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// look up the record in the referrer repository
|
||||
ReferralRecord rec = null;
|
||||
try {
|
||||
rec = repository.lookupReferral(refId);
|
||||
return (rec == null) ? OOOUser.DEFAULT_SITE_ID : (-1 * rec.referrerId);
|
||||
} catch (PersistenceException pe) {
|
||||
log.warning("Error looking up referral " + "[ref=" + referrer + ", error=" + pe
|
||||
+ "].");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// handle old-style personal referrals
|
||||
if (referrer.startsWith("user-")) {
|
||||
try {
|
||||
return -1 * Integer.parseInt(referrer.substring(5));
|
||||
} catch (NumberFormatException nfe) {
|
||||
log.warning("Bogus user referral: " + referrer);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// check referrer string against site repository
|
||||
int site = identifier.getSiteId(referrer);
|
||||
if (site != -1) {
|
||||
return site;
|
||||
}
|
||||
log.warning("Bogus site specified in referrer " + "[value=" + referrer + "].");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the contents of the 'from' parameter.
|
||||
*/
|
||||
public static String parseFromParameter (HttpServletRequest req)
|
||||
{
|
||||
String value = ParameterUtil.getParameter(req, "from", true);
|
||||
if (value != null) {
|
||||
// strip the crap some affiliates append to their ID
|
||||
Matcher am = _affregex.matcher(value);
|
||||
if (am.find()) {
|
||||
value = am.group();
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out whether the affiliate name is actually a personal referral as
|
||||
* opposed to a site referral.
|
||||
*
|
||||
* @return - True if the affiliate name is a personal referral. False if
|
||||
* it's a site or unknown.
|
||||
*/
|
||||
protected static boolean isPersonalSuffix (String name)
|
||||
{
|
||||
return name != null && name.matches("r[0-9]+");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value stored in the site id cookie.
|
||||
*/
|
||||
protected static String readSiteIdCookie (HttpServletRequest req)
|
||||
{
|
||||
return CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_ID_COOKIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and add the affiliate id and affiliate suffix to the session. No longer
|
||||
* added to the Velocity context; this is velocity-independant.
|
||||
* @param req
|
||||
* @param referrer
|
||||
* @param tagId
|
||||
*/
|
||||
protected void setAffiliateSuffix (HttpServletRequest req, String referrer, int tagId)
|
||||
{
|
||||
if (tagId != AffiliateTag.NO_TAG) {
|
||||
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer + "-" + tagId);
|
||||
} else {
|
||||
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer);
|
||||
}
|
||||
|
||||
req.getSession().setAttribute(AffiliateUtils.AFFILIATE_ID_ATTRIBUTE, referrer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a cookie to the response (velocity independant).
|
||||
* @param req servlet request object
|
||||
* @param rsp sevlet response object
|
||||
* @param name cookie identifier
|
||||
* @param value value for the cookie
|
||||
*/
|
||||
protected static void giveCookie (HttpServletRequest req, HttpServletResponse rsp,
|
||||
String name, String value)
|
||||
{
|
||||
Cookie cookie = new Cookie(name, value);
|
||||
// cookie expires in one month
|
||||
cookie.setMaxAge(30 * 24 * 60 * 60);
|
||||
// set the path and widened domain (eg www.domain.com -> .domain.com) of the cookie
|
||||
cookie.setPath("/");
|
||||
CookieUtil.widenDomain(req, cookie);
|
||||
rsp.addCookie(cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the affiliate tag from the tag parameter in the request.
|
||||
*
|
||||
* @param req - The request.
|
||||
* @return - The tag string.
|
||||
*/
|
||||
protected static String readAffiliateTag (HttpServletRequest req)
|
||||
{
|
||||
return req.getParameter("tag");
|
||||
}
|
||||
|
||||
protected static int readTagId (HttpServletRequest req)
|
||||
{
|
||||
// if they have an affiliate tag cookie, get it.
|
||||
String cookie = CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_TAG_COOKIE);
|
||||
if (cookie != null) {
|
||||
try {
|
||||
return Integer.parseInt(cookie);
|
||||
} catch (NumberFormatException e) {
|
||||
return AffiliateTag.NO_TAG;
|
||||
}
|
||||
} else {
|
||||
return AffiliateTag.NO_TAG;
|
||||
}
|
||||
}
|
||||
|
||||
/** A regexp that matches just the proper parts of an affiliate id. */
|
||||
protected static Pattern _affregex = Pattern.compile(OOOUser.SITE_STRING_REGEX);
|
||||
|
||||
/**
|
||||
* The name of the legacy cookie used to store the affiliate name passed by
|
||||
* them to us with their original request.
|
||||
*/
|
||||
protected static final String SITE_REFER_COOKIE = "site_refer";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
/**
|
||||
* Maintains a mapping from an arbitrary text tag to an integer identifier.
|
||||
*/
|
||||
public class AffiliateTag
|
||||
{
|
||||
/** Value indicating that the affiliate did not provide a tag **/
|
||||
public static final int NO_TAG = 0;
|
||||
|
||||
/** The automatically generated tag id. */
|
||||
public int tagId;
|
||||
|
||||
/** The arbitrary text tag. */
|
||||
public String tag;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
/*
|
||||
* Copyright (C) 2007-2010 Three Rings Design, Inc.
|
||||
* Copyright 1999,2004 The Apache Software Foundation.
|
||||
*/
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with affiliate tagging.
|
||||
*/
|
||||
public class AffiliateUtils {
|
||||
|
||||
/** Request parameter containing integer affiliate site id. */
|
||||
public static final String AFFILIATE_ID_PARAMETER = "affiliate";
|
||||
|
||||
/** before the days of affiliate, there was from,
|
||||
* which also supports referrals from in-game users */
|
||||
public static final String FROM_PARAMETER = "from";
|
||||
|
||||
/** Request parameter containing sub-affiliate tag; an arbitrary integer usable
|
||||
* by the partner to distinguish different placements on their site. */
|
||||
public static final String AFFILIATE_TAG_PARAMETER = "tag";
|
||||
|
||||
/** Affiliate cookie name.
|
||||
* this cookie is also used for legacy personal referrals */
|
||||
public static final String AFFILIATE_ID_COOKIE = "site_id";
|
||||
|
||||
/** Affiliate arbitrary tag cookie name. */
|
||||
public static final String AFFILIATE_TAG_COOKIE = "affiliate_tag";
|
||||
|
||||
/** The name of the session attribute used to hold the affiliate id */
|
||||
public static final String AFFILIATE_ID_ATTRIBUTE = AffiliateUtils.class.getName() + ".AffiliateId";
|
||||
|
||||
/** The name of the session attribute used to hold the affiliate tag */
|
||||
public static final String AFFILIATE_TAG_ATTRIBUTE = AffiliateUtils.class.getName() + ".AffiliateTag";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
/**
|
||||
* Represents a row in the BANNED_IDENTS table.
|
||||
*/
|
||||
public class BannedIdent
|
||||
{
|
||||
/** A 'unique' id for a specific machine we have seen. */
|
||||
public String machIdent;
|
||||
|
||||
/** The site id which this machine is banned from. */
|
||||
public int siteId;
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return "(" + siteId + ") " + machIdent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import com.samskivert.net.HttpPostUtil;
|
||||
import com.samskivert.util.Logger;
|
||||
import com.samskivert.util.ServiceWaiter;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Utility methods to verify that a user is actually a Shockwave Gameblast
|
||||
* member.
|
||||
*
|
||||
* See GameBlastRemoteAPI.htm in the docs.
|
||||
*/
|
||||
public class BlastVerify
|
||||
{
|
||||
/** Return status code indicating that the account verified. */
|
||||
public static final byte VERIFIED = 0;
|
||||
|
||||
/** Return status code indicating invalid account. */
|
||||
public static final byte INVALID_ACCOUNT = 1;
|
||||
|
||||
/** Return status code indicating an expired account. */
|
||||
public static final byte EXPIRED = 2;
|
||||
|
||||
/** Return status code indicating that there were technical difficulties
|
||||
* and the blastiness of the user cannot be determined at this time. */
|
||||
public static final byte RETRY = 3;
|
||||
|
||||
/** Returns status code indicating invalid account. */
|
||||
public static final byte INVALID_PASSWORD = 4;
|
||||
|
||||
/**
|
||||
* Verify the username/password to ensure that it's an active shockwave
|
||||
* blast user.
|
||||
*
|
||||
* @return true if the account is in good standing, false if it doesn
|
||||
*/
|
||||
public static byte verifyBlastUser (String username, String password)
|
||||
{
|
||||
String request = "member_name=" + StringUtil.encode(username) +
|
||||
"&password=" + StringUtil.encode(password);
|
||||
|
||||
String response;
|
||||
try {
|
||||
response = HttpPostUtil.httpPost(LOGIN_URL, request, TIMEOUT);
|
||||
|
||||
} catch (ServiceWaiter.TimeoutException te) {
|
||||
return RETRY;
|
||||
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Error communicating with blast", ioe);
|
||||
return RETRY;
|
||||
}
|
||||
|
||||
// we just do a very simplistic parsing of the response
|
||||
StringTokenizer st = new StringTokenizer(response);
|
||||
while (st.hasMoreTokens()) {
|
||||
String s = st.nextToken();
|
||||
if (s.startsWith("Error_Code")) {
|
||||
StringTokenizer st2 = new StringTokenizer(s, " =\"");
|
||||
if (st2.countTokens() == 2) {
|
||||
st2.nextToken();
|
||||
String code = st2.nextToken();
|
||||
try {
|
||||
int result = Integer.parseInt(code);
|
||||
switch (result) {
|
||||
case 1: return VERIFIED;
|
||||
|
||||
case 2: return INVALID_PASSWORD;
|
||||
|
||||
case 3: return EXPIRED;
|
||||
|
||||
case 4: return INVALID_ACCOUNT;
|
||||
|
||||
default: return RETRY;
|
||||
}
|
||||
|
||||
} catch (NumberFormatException nfe) {
|
||||
// handled below
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.warning("Unable to parse response from blast [resp=\"" + response + "\"].");
|
||||
return RETRY;
|
||||
}
|
||||
|
||||
/** How long we wait while trying to verify. */
|
||||
protected static final int TIMEOUT = 30;
|
||||
|
||||
/** The URL for username/password verification. */
|
||||
protected static URL LOGIN_URL;
|
||||
|
||||
/** Used for logging. */
|
||||
protected static final Logger log = Logger.getLogger(BlastVerify.class);
|
||||
|
||||
static {
|
||||
try {
|
||||
LOGIN_URL = new URL(
|
||||
// "http://gameblastbeta.shockwave.com" + // testing URL
|
||||
"https://transactor.shockwave.com" + // real URL
|
||||
"/servlet/LoginRemote");
|
||||
|
||||
} catch (MalformedURLException mue) {
|
||||
log.warning("Bad URL specification", mue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Tracks conversion related actions taken by our users.
|
||||
*/
|
||||
public class ConversionRecord
|
||||
{
|
||||
/** Indicates that an account was created. */
|
||||
public static final int CREATED = 1;
|
||||
|
||||
/** Indicates that an account that was not currently paying us started
|
||||
* to. This won't be true before 2005/03/01 where we tried (more or
|
||||
* less) to save everytime someone gave us money. */
|
||||
public static final int SUBSCRIPTION_START = 2;
|
||||
|
||||
/** Indicates that a subscription was canceled or lapsed from use. */
|
||||
public static final int SUBSCRIPTION_ENDED = 3;
|
||||
|
||||
/** The unique identifier of the user that took an action. */
|
||||
public int userId;
|
||||
|
||||
/**
|
||||
* The partner with whom the user is associated. You should always
|
||||
* use the accessor method to access this value in order to correctly
|
||||
* deal with personal referrals.
|
||||
*/
|
||||
public int siteId;
|
||||
|
||||
/** The identifier indicating the action taken. */
|
||||
public short action;
|
||||
|
||||
/** The date on which the action was taken. */
|
||||
public Timestamp recorded;
|
||||
|
||||
/**
|
||||
* Returns the site id associated with this record, properly mapping
|
||||
* negative ids (personal referrals) into a single category.
|
||||
*/
|
||||
public int getSiteId ()
|
||||
{
|
||||
return (siteId <= 0) ? OOOUser.REFERRAL_SITE_ID : siteId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
|
||||
public boolean equals (ConversionRecord cr)
|
||||
{
|
||||
return (cr.userId == userId && cr.siteId == siteId &&
|
||||
cr.action == action && cr.recorded.equals(recorded));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.Types;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.JORARepository;
|
||||
import com.samskivert.jdbc.jora.Table;
|
||||
|
||||
import com.samskivert.util.ArrayIntSet;
|
||||
import com.samskivert.util.HashIntMap;
|
||||
import com.samskivert.util.IntIntMap;
|
||||
import com.samskivert.util.Calendars;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Provides an interface to the conversion repository. A service that
|
||||
* makes use of the OOO user management and billing system can make use of
|
||||
* this conversion service to track conversion related information for
|
||||
* their users.
|
||||
*/
|
||||
public class ConversionRepository extends JORARepository
|
||||
{
|
||||
/**
|
||||
* The database identifier used when establishing a database
|
||||
* connection. This value being <code>conversiondb</code>.
|
||||
*/
|
||||
public static final String CONVERSION_DB_IDENT = "conversiondb";
|
||||
|
||||
/**
|
||||
* Creates the repository and prepares it for operation.
|
||||
*
|
||||
* @param provider the database connection provider.
|
||||
*/
|
||||
public ConversionRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, CONVERSION_DB_IDENT);
|
||||
|
||||
// figure out whether or not the forums are in use on this system
|
||||
execute(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
_active = JDBCUtil.tableExists(conn, "CONVERSION");
|
||||
if (!_active) {
|
||||
log.info("No conversion table. Disabling.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// TEMP CODE To update the repository to using a full integer for
|
||||
// siteId, as personal referrer ids are -userIds which can be
|
||||
// quite large~
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
if (!_active) {
|
||||
return null;
|
||||
}
|
||||
if (JDBCUtil.getColumnType(conn, "CONVERSION", "SITE_ID") == Types.SMALLINT) {
|
||||
JDBCUtil.changeColumn(conn, "CONVERSION", "SITE_ID",
|
||||
"SITE_ID INTEGER NOT NULL");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an action in the conversion table.
|
||||
*/
|
||||
public void recordAction (int userId, int siteId, int action)
|
||||
throws PersistenceException
|
||||
{
|
||||
recordAction(userId, siteId, action, new Timestamp(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an action in the conversion table.
|
||||
*/
|
||||
public void recordAction (int userId, int siteId, int action, Timestamp recorded)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
ConversionRecord record = new ConversionRecord();
|
||||
record.userId = userId;
|
||||
record.siteId = siteId;
|
||||
record.action = (short)action;
|
||||
record.recorded = recorded;
|
||||
insert(_ctable, record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscriber info for a given date broken up by site. Key 0
|
||||
* references a total for all sites.
|
||||
*/
|
||||
public IntIntMap getSiteSubscribers (java.util.Date date)
|
||||
{
|
||||
checkRecomputeSubInfo();
|
||||
return _subscribers.get(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total number of subscribers on then given date.
|
||||
*/
|
||||
public int getTotalSubscribers (java.util.Date date)
|
||||
{
|
||||
checkRecomputeSubInfo();
|
||||
return _subscribers.get(date).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of our subscription info. Date -> IntIntMap,
|
||||
* IntIntMap: SiteId -> Subscribers. SiteId 0 is a total of all sites.
|
||||
*/
|
||||
public Map<Date, IntIntMap> getSubscriptionInfo ()
|
||||
{
|
||||
checkRecomputeSubInfo();
|
||||
return _subscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute our subscription info if it is older than the cache time.
|
||||
*/
|
||||
protected void checkRecomputeSubInfo ()
|
||||
{
|
||||
if (_subTime + SUB_CACHE_TIME < System.currentTimeMillis()) {
|
||||
try {
|
||||
computeSubscriptionInfo();
|
||||
} catch (PersistenceException pe) {
|
||||
log.warning("Failed to compute our subscription info: " + pe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up all the subscriber data over time and affiliate.
|
||||
*/
|
||||
protected void computeSubscriptionInfo ()
|
||||
throws PersistenceException
|
||||
{
|
||||
// get all the raw data from the db, ordered by date
|
||||
List<ConversionRecord> data = loadAll(_ctable, "where ACTION in (" +
|
||||
ConversionRecord.SUBSCRIPTION_START + ", " +
|
||||
ConversionRecord.SUBSCRIPTION_ENDED + ") order by RECORDED");
|
||||
|
||||
java.util.Date recent = null;
|
||||
|
||||
ArrayIntSet subs = new ArrayIntSet();
|
||||
HashIntMap<ArrayIntSet> siteSubs = new HashIntMap<ArrayIntSet>();
|
||||
|
||||
// these are used to do the right thing if we get both a subscribe
|
||||
// and unsubscribe action in the same day, but in the wrong order
|
||||
ArrayIntSet subexcept = new ArrayIntSet();
|
||||
ArrayIntSet unsubexcept = new ArrayIntSet();
|
||||
|
||||
Iterator<ConversionRecord> itr = data.iterator();
|
||||
while (itr.hasNext()) {
|
||||
ConversionRecord cr = itr.next();
|
||||
|
||||
// we only care about the date, not the time, at which the
|
||||
// entry was recorded since we are hashing by day
|
||||
java.util.Date day = Calendars.at(cr.recorded).zeroTime().toDate();
|
||||
|
||||
// if the date changes, store the data for the most recent day
|
||||
if (recent == null) {
|
||||
recent = day;
|
||||
} else if (!recent.equals(day)) {
|
||||
subexcept.clear();
|
||||
unsubexcept.clear();
|
||||
addSubscriptionEntry(recent, subs, siteSubs);
|
||||
recent = day;
|
||||
}
|
||||
|
||||
// make sure we have a sitewise set of subscribers
|
||||
if (!siteSubs.containsKey(cr.getSiteId())) {
|
||||
siteSubs.put(cr.getSiteId(), new ArrayIntSet());
|
||||
}
|
||||
ArrayIntSet siteMap = siteSubs.get(cr.getSiteId());
|
||||
|
||||
// update our data
|
||||
if (cr.action == ConversionRecord.SUBSCRIPTION_START) {
|
||||
// handle not-subscribed -> unsubscribe -> subscribe
|
||||
if (!checkExcept(unsubexcept, subexcept,
|
||||
subs.contains(cr.userId), cr)) {
|
||||
subs.add(cr.userId);
|
||||
siteMap.add(cr.userId);
|
||||
}
|
||||
|
||||
} else if (cr.action == ConversionRecord.SUBSCRIPTION_ENDED) {
|
||||
// handle is-subscribed -> subscribe -> unsubscribe
|
||||
if (!checkExcept(subexcept, unsubexcept,
|
||||
!subs.contains(cr.userId), cr)) {
|
||||
subs.remove(cr.userId);
|
||||
siteMap.remove(cr.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add in the incomplete current day info
|
||||
addSubscriptionEntry(Calendars.now().zeroTime().toDate(), subs, siteSubs);
|
||||
|
||||
// and mark when we finished
|
||||
_subTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
protected boolean checkExcept (ArrayIntSet expect, ArrayIntSet surprise,
|
||||
boolean currentStatus, ConversionRecord cr)
|
||||
{
|
||||
// first check to see if he's in the opposite exception set; in
|
||||
// which case we clear him out and ignore this action
|
||||
if (expect.remove(cr.userId)) {
|
||||
// Log.info("Processed exception [rec=" + cr +
|
||||
// ", status=" + currentStatus + "].");
|
||||
return true;
|
||||
}
|
||||
|
||||
// next, check to see if he's already in the expected state, in which
|
||||
// case we put him into the exception set
|
||||
return currentStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the _subscriptions data for the given day.
|
||||
*/
|
||||
protected void addSubscriptionEntry (java.util.Date day, ArrayIntSet subs,
|
||||
HashIntMap<ArrayIntSet> siteSubs)
|
||||
{
|
||||
IntIntMap daysubs = new IntIntMap();
|
||||
for (Map.Entry<Integer,ArrayIntSet> entry : siteSubs.entrySet()) {
|
||||
ArrayIntSet set = entry.getValue();
|
||||
int key = entry.getKey().intValue();
|
||||
daysubs.put(key, set.size());
|
||||
}
|
||||
daysubs.put(0, subs.size());
|
||||
_subscribers.put(day, daysubs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTables ()
|
||||
{
|
||||
_ctable = new Table<ConversionRecord>(
|
||||
// this isn't the real primary key, but we never load conversion
|
||||
// records individually
|
||||
ConversionRecord.class, "CONVERSION", "RECORDED", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains subscription info about our users over time. Keys are
|
||||
* dates and the values are IntIntMaps mapping siteId -> subsrivers
|
||||
* with 0 holding the total for all sites.
|
||||
*/
|
||||
protected Map<Date, IntIntMap> _subscribers = Maps.newHashMap();
|
||||
|
||||
/** The time at which we last computed our subscriber info. */
|
||||
protected long _subTime;
|
||||
|
||||
/** The time in millis to cache our subscriber info before recomputing. */
|
||||
protected long SUB_CACHE_TIME = 1l * 60l * 60l * 1000l;
|
||||
|
||||
/** If we don't detect our tables, we deactivate ourselves. */
|
||||
protected boolean _active;
|
||||
|
||||
/** The table used to new billing actions that have taken place. */
|
||||
protected Table<ConversionRecord> _ctable;
|
||||
|
||||
/** The date (3/17/2005 in ms) after which we should complain about
|
||||
wacky conversion data. */
|
||||
protected static long FIXED_DATE = 1111090429312L;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
/**
|
||||
* Used to load detailed information about a user from the OOO user
|
||||
* database tables.
|
||||
*/
|
||||
public class DetailedUser
|
||||
{
|
||||
/** The user's assigned integer userid. */
|
||||
public int userId;
|
||||
|
||||
/** The user's chosen username. */
|
||||
public String username;
|
||||
|
||||
/** The date this record was created. */
|
||||
public Date created;
|
||||
|
||||
/** The user's email address. */
|
||||
public String email;
|
||||
|
||||
/** The user's birthday. */
|
||||
public Date birthday;
|
||||
|
||||
/** The user's gender. */
|
||||
public byte gender;
|
||||
|
||||
/** The missive provided by the user during registration. */
|
||||
public String missive;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import com.samskivert.util.ByteEnum;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Represents external authentication sources supported by OOOuser.
|
||||
*/
|
||||
public enum ExternalAuther
|
||||
implements ByteEnum
|
||||
{
|
||||
FACEBOOK(1, "facebook.com"),
|
||||
OPEN_SOCIAL(2, "opensocial.com"),
|
||||
OPEN_ID(3, "openid.net"),
|
||||
HEYZAP(4, "heyzap.com") {
|
||||
@Override public String makeEmail (String externalId) {
|
||||
return super.makeEmail(StringUtil.encode(externalId));
|
||||
}
|
||||
@Override public String getExternalId (String email) {
|
||||
return StringUtil.decode(super.getExternalId(email));
|
||||
}
|
||||
},
|
||||
STEAM(5, "steampowered.com"),
|
||||
SEGA_PASS(6, "sega.com"),
|
||||
KONGREGATE(7, "kongregate.com");
|
||||
|
||||
/**
|
||||
* Creates a fake email address given the supplied external user id.
|
||||
*/
|
||||
public String makeEmail (String externalId)
|
||||
{
|
||||
return externalId + "@" + _domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the external id associated with the given email for this auther
|
||||
*/
|
||||
public String getExternalId (String email)
|
||||
{
|
||||
int idx = email.indexOf("@");
|
||||
if (idx <= 0) {
|
||||
return email;
|
||||
}
|
||||
return email.substring(0, idx);
|
||||
}
|
||||
|
||||
// from ByteEnum
|
||||
public byte toByte ()
|
||||
{
|
||||
return _code;
|
||||
}
|
||||
|
||||
ExternalAuther (int code, String domain) {
|
||||
if (code < 1 || code > 31) { // these must fit in an int
|
||||
throw new IllegalArgumentException("Code out of byte range: " + code);
|
||||
}
|
||||
_code = (byte)code;
|
||||
_domain = domain;
|
||||
}
|
||||
|
||||
protected byte _code;
|
||||
protected String _domain;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.JORARepository;
|
||||
|
||||
/**
|
||||
* Stores gameblast aux data for the X users that actually need it, where
|
||||
* X is a small and annoying number.
|
||||
*/
|
||||
public class GameBlastAuxRepository extends JORARepository
|
||||
{
|
||||
/**
|
||||
* Constructs a new GameBlastAuxRepository.
|
||||
*/
|
||||
public GameBlastAuxRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, OOOUserRepository.USER_REPOSITORY_IDENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTables ()
|
||||
{
|
||||
// nada
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the username/password for the specified userId, or null
|
||||
* if none exists.
|
||||
*/
|
||||
public String[] getAuxData (final int userId)
|
||||
throws PersistenceException
|
||||
{
|
||||
return execute(new Operation<String[]>() {
|
||||
public String[] invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
Statement stmt = conn.createStatement();
|
||||
try {
|
||||
String query = "select LOGIN, PASSWORD from " +
|
||||
"GAMEBLAST_AUX where USER_ID = " + userId;
|
||||
|
||||
ResultSet rs = stmt.executeQuery(query);
|
||||
if (rs.next()) {
|
||||
return new String[] { rs.getString(1),
|
||||
rs.getString(2) };
|
||||
}
|
||||
return null; // didn't find
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the specified gameblast login is is not present or if it is,
|
||||
* only belongs to the specified user.
|
||||
*/
|
||||
public boolean ensureLoginUnique (final String login, final int userId)
|
||||
throws PersistenceException
|
||||
{
|
||||
Boolean boolval = execute(new Operation<Boolean>() {
|
||||
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
PreparedStatement stmt = null;
|
||||
try {
|
||||
stmt = conn.prepareStatement("select USER_ID from " +
|
||||
"GAMEBLAST_AUX where LOGIN=?");
|
||||
stmt.setString(1, login);
|
||||
ResultSet rs = stmt.executeQuery();
|
||||
|
||||
// return true if there are no matches or if the first
|
||||
// one is us. (There should never be two, anyway).
|
||||
return Boolean.valueOf(
|
||||
!rs.next() || (rs.getInt(1) == userId));
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
}
|
||||
});
|
||||
return boolval.booleanValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the data for the specified user.
|
||||
*/
|
||||
public void saveAuxData (final int userId,
|
||||
final String login, final String password)
|
||||
throws PersistenceException
|
||||
{
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
PreparedStatement stmt = null;
|
||||
try {
|
||||
// try updating first
|
||||
stmt = conn.prepareStatement(
|
||||
"update GAMEBLAST_AUX set " +
|
||||
"LOGIN=?, PASSWORD=? where USER_ID=?");
|
||||
stmt.setString(1, login);
|
||||
stmt.setString(2, password);
|
||||
stmt.setInt(3, userId);
|
||||
if (stmt.executeUpdate() != 1) {
|
||||
// insert a new one
|
||||
PreparedStatement stmt2 = null;
|
||||
try {
|
||||
stmt2 = conn.prepareStatement(
|
||||
"insert into GAMEBLAST_AUX " +
|
||||
"(USER_ID, LOGIN, PASSWORD) values (?, ?, ?)");
|
||||
stmt2.setInt(1, userId);
|
||||
stmt2.setString(2, login);
|
||||
stmt2.setString(3, password);
|
||||
JDBCUtil.checkedUpdate(stmt2, 1);
|
||||
} finally {
|
||||
JDBCUtil.close(stmt2);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the gameblast mapping for the specified user.
|
||||
*/
|
||||
public void removeAuxData (final int userId)
|
||||
throws PersistenceException
|
||||
{
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
Statement stmt = conn.createStatement();
|
||||
try {
|
||||
String query = "delete from GAMEBLAST_AUX where USER_ID=" +
|
||||
userId;
|
||||
stmt.executeUpdate(query);
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
/**
|
||||
* Retains information about a historical user registration.
|
||||
*/
|
||||
public class HistoricalUser
|
||||
{
|
||||
/** The user's assigned integer userid. */
|
||||
public int userId;
|
||||
|
||||
/** The user's chosen username. */
|
||||
public String username;
|
||||
|
||||
/** The date this record was created. */
|
||||
public Date created;
|
||||
|
||||
/** The affiliate site with which this user is associated. */
|
||||
public int siteId;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import com.samskivert.util.Logger;
|
||||
|
||||
/**
|
||||
* Contains a reference to the log object used by the user package.
|
||||
*/
|
||||
public class Log
|
||||
{
|
||||
/** The log instance that will be used to log all messages for the user package. */
|
||||
public static Logger log = Logger.getLogger("com.threerings.user");
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import com.google.common.collect.HashMultiset;
|
||||
import com.google.common.collect.Multiset;
|
||||
|
||||
import com.samskivert.util.Interval;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Tracks logins by a user identifier and if they attempt to login too often, lets us know.
|
||||
* Note that the period is how often we clear the entire table, so specific users may find
|
||||
* up to 2x the attempt rate during their first set of attempts if it happens to cross the
|
||||
* boundary.
|
||||
*/
|
||||
public class LoginThrottle<K>
|
||||
{
|
||||
public LoginThrottle (int maxLogins, long period)
|
||||
{
|
||||
_maxLogins = maxLogins;
|
||||
|
||||
(new Interval() {
|
||||
@Override
|
||||
public void expired ()
|
||||
{
|
||||
synchronized(_recentLogins) {
|
||||
_recentLogins.clear();
|
||||
}
|
||||
}
|
||||
}).schedule(period, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes that the user attempted to login.
|
||||
* @return Whether they're allowed to continue based on throttle settings.
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean noteLogin (K userIdentifier)
|
||||
{
|
||||
synchronized(_recentLogins) {
|
||||
int loginCount = 1 + _recentLogins.add(userIdentifier, 1);
|
||||
if (loginCount > _maxLogins) {
|
||||
recordThrottledAttempt(userIdentifier, loginCount);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes that the user successfully logged in (so reduces their count by 1).
|
||||
*/
|
||||
@Deprecated
|
||||
public void noteLoginSuccess (K userIdentifier)
|
||||
{
|
||||
synchronized(_recentLogins) {
|
||||
_recentLogins.add(userIdentifier, -1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we block a login attempt because the user has already failed too many times?
|
||||
*/
|
||||
public boolean isLoginAttemptBlocked (K userIdentifier)
|
||||
{
|
||||
int count;
|
||||
synchronized (_recentLogins) {
|
||||
count = _recentLogins.count(userIdentifier);
|
||||
}
|
||||
if (_maxLogins <= count) {
|
||||
recordThrottledAttempt(userIdentifier, count);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note a failed login, and return true if it's time to tell them that we'll block
|
||||
* them for a little while.
|
||||
*/
|
||||
public boolean noteFailedLogin (K userIdentifier)
|
||||
{
|
||||
synchronized (_recentLogins) {
|
||||
return _maxLogins <= 1 + _recentLogins.add(userIdentifier, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log that we had a failed attempt. Can be overridden if you really don't care about tracking
|
||||
* that sort of thing.
|
||||
*/
|
||||
protected void recordThrottledAttempt (K userIdentifier, int loginCount)
|
||||
{
|
||||
log.info("Throttled login attempt", "identifier", userIdentifier, "loginCount", loginCount);
|
||||
}
|
||||
|
||||
/** How many logins they're allowed to try during a period. */
|
||||
protected int _maxLogins;
|
||||
|
||||
/** Recent login attempt counts by user identifier. */
|
||||
protected Multiset<K> _recentLogins = HashMultiset.create();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Auxiliary information relating to our registered users.
|
||||
*/
|
||||
public class OOOAuxData
|
||||
{
|
||||
/** The user's unique identifier. */
|
||||
public int userId;
|
||||
|
||||
/** The user's birthday. */
|
||||
public Date birthday;
|
||||
|
||||
/** The user's gender. */
|
||||
public byte gender;
|
||||
|
||||
/** The user's personal missive to us. */
|
||||
public String missive;
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
/**
|
||||
* Contains additional information on users that have paid us in one form or
|
||||
* another.
|
||||
*/
|
||||
public class OOOBillAuxData
|
||||
{
|
||||
/** The user's unique identifier. */
|
||||
public int userId;
|
||||
|
||||
/** The first time the user bought coins **/
|
||||
public Timestamp firstCoinBuy;
|
||||
|
||||
/** The most recent time the user bought coins **/
|
||||
public Timestamp latestCoinBuy;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import com.samskivert.servlet.Site;
|
||||
|
||||
/**
|
||||
* Used to identify OOO sites.
|
||||
*/
|
||||
public class OOOSite extends Site
|
||||
{
|
||||
/** The domain at which this site is found, e.g. puzzlepirates.com */
|
||||
public final String domain;
|
||||
|
||||
public OOOSite (int siteId, String siteString, String domain)
|
||||
{
|
||||
super(siteId, siteString);
|
||||
this.domain = domain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.servlet.JDBCTableSiteIdentifier;
|
||||
import com.samskivert.servlet.Site;
|
||||
import com.samskivert.servlet.util.CookieUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Handles the identification of OOO sites without requiring us to maintain a domains everywhere we
|
||||
* use our webapps. It still uses the "sites" table for affiliate handling, so we can set up
|
||||
* affiliates using the register webapp and whatnot, but it uses a static mapping (defined in
|
||||
* OOOUser) for our domains and site ids.
|
||||
*/
|
||||
public class OOOSiteIdentifier extends JDBCTableSiteIdentifier
|
||||
{
|
||||
/** A request property that can be used to override the domain-based site identification. See
|
||||
* {@link HttpServletRequest#setAttribute}. */
|
||||
public static final String SITE_ID_OVERRIDE_KEY = "SiteIdentifierOverride";
|
||||
|
||||
/** A cookie that can be provided with a request to override the domain-based site
|
||||
* identification. This is superceded by {@link #SITE_ID_OVERRIDE_KEY}. */
|
||||
public static final String SITE_COOKIE = "site";
|
||||
|
||||
public OOOSiteIdentifier (ConnectionProvider conprov)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(conprov);
|
||||
}
|
||||
|
||||
@Override // from JDBCTableSiteIdentifier
|
||||
public int identifySite (HttpServletRequest req)
|
||||
{
|
||||
// look for request parameter, accept it as an override and store for the session
|
||||
String requestParam = req.getParameter(SITE_ID_OVERRIDE_KEY);
|
||||
if (requestParam != null) {
|
||||
try {
|
||||
Integer siteId = Integer.parseInt(requestParam);
|
||||
req.getSession().setAttribute(SITE_ID_OVERRIDE_KEY, siteId);
|
||||
return siteId;
|
||||
} catch (Exception e) {
|
||||
log.warning("Received invalid site override param", "site", requestParam);
|
||||
// fall through to other methods
|
||||
}
|
||||
}
|
||||
|
||||
// check for override in the request attributes and store in the session
|
||||
Integer attributeOverride = (Integer)req.getAttribute(SITE_ID_OVERRIDE_KEY);
|
||||
if (attributeOverride != null) {
|
||||
req.getSession().setAttribute(SITE_ID_OVERRIDE_KEY, attributeOverride);
|
||||
return attributeOverride;
|
||||
}
|
||||
|
||||
// check whether our site id was overridden during this session
|
||||
Integer override = (Integer)req.getSession().getAttribute(SITE_ID_OVERRIDE_KEY);
|
||||
if (override != null) {
|
||||
return override;
|
||||
}
|
||||
|
||||
// check for a site cookie
|
||||
String sitecookie = CookieUtil.getCookieValue(req, SITE_COOKIE);
|
||||
if (!StringUtil.isBlank(sitecookie)) {
|
||||
try {
|
||||
return Integer.parseInt(sitecookie);
|
||||
} catch (Exception e) {
|
||||
log.warning("Received invalid site cookie", "site", sitecookie);
|
||||
// fall through to the domain parsing
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise we just use a static mapping
|
||||
String serverName = req.getServerName();
|
||||
for (OOOSite site : OOOUser.SITES) {
|
||||
if (serverName.endsWith(site.domain)) {
|
||||
return site.siteId;
|
||||
}
|
||||
}
|
||||
return super.identifySite(req);
|
||||
}
|
||||
|
||||
@Override // from JDBCTableSiteIdentifier
|
||||
public String getSiteString (int siteId)
|
||||
{
|
||||
for (OOOSite site : OOOUser.SITES) {
|
||||
if (site.siteId == siteId) {
|
||||
return site.siteString;
|
||||
}
|
||||
}
|
||||
return super.getSiteString(siteId);
|
||||
}
|
||||
|
||||
@Override // from JDBCTableSiteIdentifier
|
||||
public int getSiteId (String siteString)
|
||||
{
|
||||
for (OOOSite site : OOOUser.SITES) {
|
||||
if (site.siteString.equals(siteString)) {
|
||||
return site.siteId;
|
||||
}
|
||||
}
|
||||
return super.getSiteId(siteString);
|
||||
}
|
||||
|
||||
@Override // from JDBCTableSiteIdentifier
|
||||
public Iterator<Site> enumerateSites ()
|
||||
{
|
||||
List<Site> sites = Lists.newArrayList(super.enumerateSites());
|
||||
for (OOOSite site : OOOUser.SITES) {
|
||||
if (!_sitesById.containsKey(site.siteId)) {
|
||||
sites.add(site);
|
||||
}
|
||||
}
|
||||
return sites.iterator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import com.samskivert.jdbc.jora.FieldMask;
|
||||
import com.samskivert.servlet.user.User;
|
||||
import com.samskivert.util.ArrayIntSet;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Extends the basic samskivert user record with special Three Rings business.
|
||||
*/
|
||||
public class OOOUser extends User
|
||||
{
|
||||
/** The flag set when the user's e-mail address has been validated. */
|
||||
public static final int VALIDATED_FLAG = (1 << 0);
|
||||
|
||||
/** A flag set when a user has opted-in to receive partner site spam. */
|
||||
public static final int AFFILIATE_SPAM_FLAG = (1 << 1);
|
||||
|
||||
/** A flag set when a user pays us money to buy coins. */
|
||||
public static final int HAS_BOUGHT_COINS_FLAG = (1 << 2);
|
||||
|
||||
/** A flag set when a user redeems a ubisoft cd key. */
|
||||
public static final int UBISOFT_KEY_REDEEMED_FLAG = (1 << 3);
|
||||
|
||||
/** A flag set when a user pays us money for bulk time. */
|
||||
public static final int HAS_BOUGHT_TIME_FLAG = (1 << 4);
|
||||
|
||||
/** Indicates that a user is an active player of Y!PP. */
|
||||
public static final int IS_ACTIVE_YOHOHO_PLAYER = (1 << 5);
|
||||
|
||||
/** Indicates that a user is an active player of B!H. */
|
||||
public static final int IS_ACTIVE_BANG_PLAYER = (1 << 6);
|
||||
|
||||
/** Indicates that a user is an active player of GG. */
|
||||
public static final int IS_ACTIVE_GARDENS_PLAYER = (1 << 7);
|
||||
|
||||
/** Indicates that the user has an active subscription on the family friendly servers */
|
||||
public static final int FAMILY_SUBSCRIBER = (1 << 8);
|
||||
|
||||
/** Indicates that the user has been involved in a conversion to a Steam account (either as the
|
||||
* source or the destination of the conversion). */
|
||||
public static final int CONVERTED_TO_STEAM = (1 << 9);
|
||||
|
||||
/** An access token indicating that this user is an admin. */
|
||||
public static final byte ADMIN = 1;
|
||||
|
||||
/** An access token indicating that this user is a maintainer. */
|
||||
public static final byte MAINTAINER = 2;
|
||||
|
||||
/** An access token indicating that this user is an insider. */
|
||||
public static final byte INSIDER = 3;
|
||||
|
||||
/** An access token indicating that this user is a tester. */
|
||||
public static final byte TESTER = 4;
|
||||
|
||||
/** An access token indicating that this user is banned from Puzzle Pirates. */
|
||||
public static final byte PP_BANNED = 5;
|
||||
|
||||
/** An access token indicating that this user is customer support personnel. */
|
||||
public static final byte SUPPORT = 6;
|
||||
|
||||
/** An access token allowing them to spend more than the default max. */
|
||||
public static final byte BIG_SPENDER = 7;
|
||||
|
||||
/** An access token indicating that the user has bounced a check or reversed a payment for
|
||||
* Puzzle Pirates. */
|
||||
public static final byte PP_DEADBEAT = 8;
|
||||
|
||||
/** An access token indicating that this user is banned from Bang! Howdy. */
|
||||
public static final byte BANG_BANNED = 9;
|
||||
|
||||
/** An access token indicating that the user has bounced a check or reversed a payment for
|
||||
* Bang! Howdy. */
|
||||
public static final byte BANG_DEADBEAT = 10;
|
||||
|
||||
/** An access token indicating that this user is banned from MetaSOY. */
|
||||
public static final byte MSOY_BANNED = 11;
|
||||
|
||||
/** An access token indicating that the user has bounced a check or reversed a payment for
|
||||
* MetaSOY. */
|
||||
public static final byte MSOY_DEADBEAT = 12;
|
||||
|
||||
/** An access token indicating that this user is banned from an app. */
|
||||
public static final byte APPS_BANNED = 13;
|
||||
|
||||
/** An access token indicating that the user has bounced a check or reversed a payment for an
|
||||
* app. */
|
||||
public static final byte APPS_DEADBEAT = 14;
|
||||
|
||||
/** An access token indicating that the user is banned from Project X. */
|
||||
public static final byte PROJECTX_BANNED = 15;
|
||||
|
||||
/** An access token indicating that the user has bounced a check or reversed a payment for
|
||||
* Project X. */
|
||||
public static final byte PROJECTX_DEADBEAT = 16;
|
||||
|
||||
/** Billing status flags for a particular service. */
|
||||
public static final byte TRIAL_STATE = 0;
|
||||
public static final byte SUBSCRIBER_STATE = 1;
|
||||
public static final byte BILLING_FAILURE_STATE = 2;
|
||||
public static final byte EX_SUBSCRIBER_STATE = 3;
|
||||
public static final byte BANNED_STATE = 4;
|
||||
|
||||
/** A regular expression that defines the valid characters for affiliate site identifier
|
||||
* strings. */
|
||||
public static final String SITE_STRING_REGEX = "[-._A-Za-z0-9]+";
|
||||
|
||||
/** The default site id to use in the absence of others; currently puzzlepirates.com.
|
||||
* Eventually we'll have to have something on a per-domain basis. */
|
||||
public static final int DEFAULT_SITE_ID = 2;
|
||||
|
||||
/** The puzzlepirates.com site identifier. */
|
||||
public static final int PUZZLEPIRATES_SITE_ID = 2;
|
||||
|
||||
/** affiliate site id used for "alt" accounts created through pp.com/register/. */
|
||||
public static final int PUZZLEPIRATES_ALT_SITE_ID = 164;
|
||||
|
||||
/** The gamegardens.com site identifier. */
|
||||
public static final int GAMEGARDENS_SITE_ID = 6;
|
||||
|
||||
/** The site id we group personal referrers under when doing reporting. This should never be
|
||||
* inserted into the database. */
|
||||
public static final int REFERRAL_SITE_ID = 7;
|
||||
|
||||
/** The banghowdy.com site identifier. */
|
||||
public static final int BANGHOWDY_SITE_ID = 8;
|
||||
|
||||
/** The whirled.com site identifier. */
|
||||
public static final int METASOY_SITE_ID = 9;
|
||||
|
||||
/** The puzzlepiratesfamily.com site identifier */
|
||||
public static final int YPPFAMILY_SITE_ID = 10;
|
||||
|
||||
/** The apps.threerings.net site identifier. */
|
||||
public static final int APPS_SITE_ID = 11;
|
||||
|
||||
/** The Everything app site identifier. */
|
||||
public static final int EVERYTHING_SITE_ID = 12;
|
||||
|
||||
/** The BiteMe app site identifier. */
|
||||
public static final int BITEME_SITE_ID = 13;
|
||||
|
||||
/** The Down Town app site identifier. */
|
||||
public static final int DOWNTOWN_SITE_ID = 14;
|
||||
|
||||
/** The Face Pirate app site identifier. */
|
||||
public static final int FACEPIRATE_SITE_ID = 15;
|
||||
|
||||
/** The ProjectX app site identifier - not yet used for billing, only register. */
|
||||
public static final int PROJECTX_SITE_ID = 204;
|
||||
|
||||
/** The Who site identifier - jumping to 1000 to avoid collisions with YPP and SK affiliates */
|
||||
public static final int WHO_SITE_ID = 1000;
|
||||
|
||||
/** The Ubisoft site id, which we need for various hackery. */
|
||||
public static final int UBISOFT_SITE_ID = 40;
|
||||
|
||||
/** A mapping from domain to site id for the OOO sites. */
|
||||
public static List<OOOSite> SITES = ImmutableList.of(
|
||||
new OOOSite(PUZZLEPIRATES_SITE_ID, "puzzlepirates", "puzzlepirates.com"),
|
||||
new OOOSite(GAMEGARDENS_SITE_ID, "gardens", "gamegardens.com"),
|
||||
new OOOSite(BANGHOWDY_SITE_ID, "bang", "banghowdy.com"),
|
||||
new OOOSite(METASOY_SITE_ID, "metasoy", "whirled.com"),
|
||||
new OOOSite(YPPFAMILY_SITE_ID, "family", "puzzlepiratesfamily.com"),
|
||||
new OOOSite(APPS_SITE_ID, "apps", "apps.threerings.net"),
|
||||
new OOOSite(EVERYTHING_SITE_ID, "everything", "notused"),
|
||||
new OOOSite(BITEME_SITE_ID, "biteme", "notused"),
|
||||
new OOOSite(DOWNTOWN_SITE_ID, "downtown", "notused"),
|
||||
new OOOSite(PROJECTX_SITE_ID, "projectx", "spiralknights.com"));
|
||||
|
||||
/** The subscriber column name for Puzzle Pirates subscribers. Used by various repository
|
||||
* methods. */
|
||||
public static final String PUZZLEPIRATES_COLUMN = "yohoho";
|
||||
|
||||
/** Used to make sure someone doesn't do something stupid. */
|
||||
public static final String[] IDENTS_NOT_LOADED = {};
|
||||
|
||||
/** The flags detailing the user's various bits of status. (VALIDATED_FLAG, etc) */
|
||||
public int flags;
|
||||
|
||||
/** The tokens detailing the user's site access permissions. (ADMIN, TESTER, etc) */
|
||||
public byte[] tokens;
|
||||
|
||||
/** The user's account status for Yohoho! Puzzle Pirates. (TRIAL_STATE, SUBSCRIBER_STATE,
|
||||
* etc) */
|
||||
public byte yohoho;
|
||||
|
||||
/** The spots that have been given to the user by various crews. */
|
||||
public String spots;
|
||||
|
||||
/** The amount of time remaining on the users shun, in minutes. */
|
||||
public int shunLeft;
|
||||
|
||||
/** The id of any opaque tag provided by the affiliate to tag this user for their purposes. */
|
||||
public int affiliateTagId;
|
||||
|
||||
/** The list of machine identifiers associated with this user. */
|
||||
public transient String[] machIdents = IDENTS_NOT_LOADED;
|
||||
|
||||
/**
|
||||
* Returns the banned token for the site or 0 if an invalid site.
|
||||
*/
|
||||
public static byte getBannedToken (int site)
|
||||
{
|
||||
switch (site) {
|
||||
case YPPFAMILY_SITE_ID:
|
||||
case PUZZLEPIRATES_SITE_ID: return PP_BANNED;
|
||||
case BANGHOWDY_SITE_ID: return BANG_BANNED;
|
||||
case METASOY_SITE_ID: return MSOY_BANNED;
|
||||
case EVERYTHING_SITE_ID: return APPS_BANNED;
|
||||
case BITEME_SITE_ID: return APPS_BANNED;
|
||||
case DOWNTOWN_SITE_ID: return APPS_BANNED;
|
||||
case FACEPIRATE_SITE_ID: return APPS_BANNED;
|
||||
case PROJECTX_SITE_ID: return PROJECTX_BANNED;
|
||||
default: return (byte)0; // no other sites currently support banning
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deadbeat token for the site or 0 if an unsupported site.
|
||||
*/
|
||||
public static byte getDeadbeatToken (int site)
|
||||
{
|
||||
switch (site) {
|
||||
case YPPFAMILY_SITE_ID:
|
||||
case PUZZLEPIRATES_SITE_ID: return PP_DEADBEAT;
|
||||
case BANGHOWDY_SITE_ID: return BANG_DEADBEAT;
|
||||
case METASOY_SITE_ID: return MSOY_DEADBEAT;
|
||||
case EVERYTHING_SITE_ID: return APPS_DEADBEAT;
|
||||
case BITEME_SITE_ID: return APPS_DEADBEAT;
|
||||
case DOWNTOWN_SITE_ID: return APPS_DEADBEAT;
|
||||
case FACEPIRATE_SITE_ID: return APPS_DEADBEAT;
|
||||
case PROJECTX_SITE_ID: return PROJECTX_DEADBEAT;
|
||||
default:
|
||||
log.warning("Requested deadbeat token for unsupported site", "site", site);
|
||||
return (byte)0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user's e-mail address has been validated.
|
||||
*/
|
||||
public boolean isValidated ()
|
||||
{
|
||||
return isFlagSet(VALIDATED_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user has even purchased coins from us.
|
||||
*/
|
||||
public boolean hasBoughtCoins ()
|
||||
{
|
||||
return isFlagSet(HAS_BOUGHT_COINS_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user has even purchased time from us.
|
||||
*/
|
||||
public boolean hasBoughtTime ()
|
||||
{
|
||||
return isFlagSet(HAS_BOUGHT_TIME_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the specified flag has been set.
|
||||
*/
|
||||
public boolean isFlagSet (int flag)
|
||||
{
|
||||
return ((flags & flag) != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's e-mail validation status.
|
||||
*/
|
||||
public void setValidated (boolean validated)
|
||||
{
|
||||
setFlag(VALIDATED_FLAG, validated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user is flagged as a family ocean subscriber.
|
||||
*/
|
||||
public boolean isFamilySubscriber ()
|
||||
{
|
||||
return isFlagSet(OOOUser.FAMILY_SUBSCRIBER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or clear the specified flag.
|
||||
*/
|
||||
public void setFlag (int flag, boolean set)
|
||||
{
|
||||
if (set) {
|
||||
this.flags |= flag;
|
||||
} else {
|
||||
this.flags &= ~flag;
|
||||
}
|
||||
setModified("flags");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the supplied access token to this user's token ring.
|
||||
*/
|
||||
public void addToken (byte token)
|
||||
{
|
||||
// check to see if they already have it
|
||||
if (!holdsToken(token)) {
|
||||
if (tokens == null) {
|
||||
tokens = new byte[] { token };
|
||||
} else {
|
||||
int tcount = tokens.length;
|
||||
byte[] ntokens = new byte[tcount+1];
|
||||
System.arraycopy(tokens, 0, ntokens, 0, tcount);
|
||||
ntokens[tcount] = token;
|
||||
tokens = ntokens;
|
||||
}
|
||||
setModified("tokens");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all the spots that this user has recieved.
|
||||
*/
|
||||
public void setSpots (ArrayIntSet blackspots)
|
||||
{
|
||||
if (blackspots == null) {
|
||||
throw new IllegalArgumentException("Blackspots parameter can not be null");
|
||||
}
|
||||
String newspots = "";
|
||||
Iterator<Integer> itr = blackspots.iterator();
|
||||
for (int ii = 0; itr.hasNext(); ii++) {
|
||||
Integer crewid = itr.next();
|
||||
if (ii == 0) {
|
||||
newspots += crewid;
|
||||
} else {
|
||||
newspots += ":" + crewid;
|
||||
}
|
||||
}
|
||||
spots = newspots;
|
||||
|
||||
setModified("spots");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the String representation of the users black spots to an ArrayIntSet. Each spot is,
|
||||
* in fact, the crewid of the crew that gave it to them.
|
||||
*/
|
||||
public ArrayIntSet getSpots ()
|
||||
{
|
||||
ArrayIntSet blackSpots = new ArrayIntSet();
|
||||
if (spots == null) {
|
||||
return blackSpots;
|
||||
}
|
||||
|
||||
StringTokenizer tok = new StringTokenizer(spots, ":");
|
||||
|
||||
while (tok.hasMoreTokens()) {
|
||||
String crewid = tok.nextToken();
|
||||
try {
|
||||
int spot = Integer.parseInt(crewid);
|
||||
blackSpots.add(spot);
|
||||
} catch (NumberFormatException nfe) {
|
||||
log.warning("Failed parsing spots.",
|
||||
"user", username, "crewid", crewid, "excpetion", nfe);
|
||||
}
|
||||
}
|
||||
|
||||
return blackSpots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the supplied access token from this user's token ring.
|
||||
*/
|
||||
public void removeToken (byte token)
|
||||
{
|
||||
// make sure they actually have it
|
||||
if (holdsToken(token)) {
|
||||
// the tokens array is likely to always be very small, so we
|
||||
// don't go to the trouble of trying to do this with arraycopy
|
||||
int tcount = tokens.length;
|
||||
byte[] ntokens = new byte[tcount-1];
|
||||
for (int ii = 0, npos = 0; ii < tcount; ii++) {
|
||||
if (tokens[ii] == token) {
|
||||
continue;
|
||||
}
|
||||
ntokens[npos++] = tokens[ii];
|
||||
}
|
||||
tokens = ntokens;
|
||||
setModified("tokens");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user holds the specified token.
|
||||
*/
|
||||
public boolean holdsToken (byte token)
|
||||
{
|
||||
if (tokens == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int tcount = tokens.length;
|
||||
for (int ii = 0; ii < tcount; ii++) {
|
||||
if (tokens[ii] == token) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the billing status of the user for a particular site.
|
||||
*
|
||||
* @return true if the status changed, false if not.
|
||||
*/
|
||||
public boolean setBillingStatus (int site, byte status)
|
||||
{
|
||||
switch (site) {
|
||||
case METASOY_SITE_ID:
|
||||
// we maintain a separate OOOUSER installation for msoy and thus rather than adding
|
||||
// another column to maintain our subscriber status for msoy, we just reuse yohoho's
|
||||
case PUZZLEPIRATES_SITE_ID:
|
||||
case YPPFAMILY_SITE_ID:
|
||||
if (yohoho != status) {
|
||||
yohoho = status;
|
||||
setModified("yohoho");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Tried to set billing status for unknown site [site=" + site + "].");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the billing status for the passed in site.
|
||||
*/
|
||||
public byte getBillingStatus (int site)
|
||||
{
|
||||
switch (site) {
|
||||
case METASOY_SITE_ID: // see setBillingStatus
|
||||
case PUZZLEPIRATES_SITE_ID:
|
||||
return yohoho;
|
||||
case YPPFAMILY_SITE_ID:
|
||||
return isFamilySubscriber() ? SUBSCRIBER_STATE : yohoho;
|
||||
default:
|
||||
return TRIAL_STATE;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMaintainer ()
|
||||
{
|
||||
return holdsToken(MAINTAINER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAdmin ()
|
||||
{
|
||||
return holdsToken(ADMIN) || isMaintainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user is an "insider" (and as such, should be allowed in for free)
|
||||
*/
|
||||
public boolean isInsider ()
|
||||
{
|
||||
return (holdsToken(INSIDER) || isAdmin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user holds the support token
|
||||
*/
|
||||
public boolean isSupport ()
|
||||
{
|
||||
return holdsToken(SUPPORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user holds the support token (or higher)
|
||||
*/
|
||||
public boolean isSupportPlus ()
|
||||
{
|
||||
return isSupport() || isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user is subscriber to Puzzle Pirates.
|
||||
*/
|
||||
public boolean isSubscriber ()
|
||||
{
|
||||
return isSubscriber(PUZZLEPIRATES_SITE_ID);
|
||||
}
|
||||
|
||||
public boolean isSubscriber (int site)
|
||||
{
|
||||
return ((getBillingStatus(site) == SUBSCRIBER_STATE) || isInsider());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we have allowed the user to be a big spender.
|
||||
*/
|
||||
public boolean isBigSpender ()
|
||||
{
|
||||
return holdsToken(BIG_SPENDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user is banned.
|
||||
*/
|
||||
public boolean isBanned (int site)
|
||||
{
|
||||
byte token = getBannedToken(site);
|
||||
return (token == 0 ? false : holdsToken(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures this user's banned status for the specified site.
|
||||
*/
|
||||
public boolean setBanned (int site, boolean banned)
|
||||
{
|
||||
byte token = getBannedToken(site);
|
||||
if (token == 0) {
|
||||
log.warning("Requested to update banned for invalid site", "site", site);
|
||||
return false;
|
||||
}
|
||||
if (banned) {
|
||||
addToken(token);
|
||||
} else {
|
||||
removeToken(token);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user has bounced a check or reversed a payment.
|
||||
*/
|
||||
public boolean isDeadbeat (int site)
|
||||
{
|
||||
byte token = getDeadbeatToken(site);
|
||||
return (token == 0 ? false : holdsToken(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures this user's deadbeat status for the specified site.
|
||||
*/
|
||||
public void setDeadbeat (int site, boolean deadbeat)
|
||||
{
|
||||
byte token = getDeadbeatToken(site);
|
||||
if (token != 0) {
|
||||
if (deadbeat) {
|
||||
addToken(token);
|
||||
} else {
|
||||
removeToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns our Yohoho! billing status.
|
||||
*/
|
||||
public byte getYohohoStatus ()
|
||||
{
|
||||
return getBillingStatus(PUZZLEPIRATES_SITE_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this user as a non paying user.
|
||||
*/
|
||||
public void makeTrialYohoho ()
|
||||
{
|
||||
setBillingStatus(PUZZLEPIRATES_SITE_ID, TRIAL_STATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user holds any of the specified tokens.
|
||||
*/
|
||||
public boolean holdsAnyToken (byte[] tokset)
|
||||
{
|
||||
if (tokens == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int tcount = tokens.length, scount = tokset.length;
|
||||
for (int ii = 0; ii < tcount; ii++) {
|
||||
for (int tt = 0; tt < scount; tt++) {
|
||||
if (tokens[ii] == tokset[tt]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// A protected method can be called by another class in the same package, but if you extend the
|
||||
// class containing the protected method and try to call that method from a class in the same
|
||||
// package as the derived class, it doesn't work. However, if we simply override the method and
|
||||
// do nothing but call <code>super</code>, it's considered "legal." Yay!
|
||||
@Override
|
||||
protected void setDirtyMask (FieldMask mask)
|
||||
{
|
||||
super.setDirtyMask(mask);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
|
||||
/**
|
||||
* Identifying information and flags for a {@link OOOUser} object. This is for use when querying
|
||||
* a potentially large number of users meeting some criteria and only each user's name and flags
|
||||
* are required.
|
||||
*/
|
||||
public class OOOUserCard
|
||||
{
|
||||
/** Converts a card to a username. */
|
||||
public static Function<OOOUserCard, String> TO_USERNAME = new Function<OOOUserCard, String>() {
|
||||
public String apply (OOOUserCard card) {
|
||||
return card.username;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new user card with the given field values.
|
||||
*/
|
||||
public OOOUserCard (int userid, String username, int flags)
|
||||
{
|
||||
this.userid = userid;
|
||||
this.username = username;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user card for deserialization.
|
||||
*/
|
||||
public OOOUserCard ()
|
||||
{
|
||||
}
|
||||
|
||||
/** The unique id of the user. */
|
||||
public int userid;
|
||||
|
||||
/** The name of the user. */
|
||||
public String username;
|
||||
|
||||
/** The {@link OOOUser#flags} of the user. */
|
||||
public int flags;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.util.RunQueue;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
|
||||
import com.samskivert.servlet.RedirectException;
|
||||
|
||||
import com.samskivert.servlet.user.User;
|
||||
import com.samskivert.servlet.user.UserManager;
|
||||
import com.samskivert.servlet.user.UserRepository;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Extends the standard user manager with OOO-specific support.
|
||||
*/
|
||||
public class OOOUserManager extends UserManager
|
||||
{
|
||||
/**
|
||||
* Creates our OOO User manager and prepares it for operation.
|
||||
*/
|
||||
public OOOUserManager (Properties config, ConnectionProvider conprov)
|
||||
throws PersistenceException
|
||||
{
|
||||
this(config, conprov, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates our OOO user manager and prepares it for operation.
|
||||
*/
|
||||
public OOOUserManager (Properties config, ConnectionProvider conprov, RunQueue pruneQueue)
|
||||
throws PersistenceException
|
||||
{
|
||||
// legacy business
|
||||
init(config, conprov, pruneQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an OOO user manager which must subsequently be initialized with
|
||||
* a call to {@link #init}.
|
||||
*/
|
||||
public OOOUserManager ()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public OOOUserRepository getRepository ()
|
||||
{
|
||||
return (OOOUserRepository)_repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init (Properties config, ConnectionProvider conprov, RunQueue pruneQueue)
|
||||
throws PersistenceException
|
||||
{
|
||||
super.init(config, conprov, pruneQueue);
|
||||
|
||||
// create the blast repository
|
||||
_blastRepo = new GameBlastAuxRepository(conprov);
|
||||
|
||||
// look up the access denied URL
|
||||
_accessDeniedURL = config.getProperty("access_denied_url");
|
||||
if (_accessDeniedURL == null) {
|
||||
log.warning("No 'access_denied_url' supplied in user manager config. " +
|
||||
"Restricted pages will behave strangely.");
|
||||
}
|
||||
|
||||
// load up our affiliate tag mappings
|
||||
for (AffiliateTag sub : getRepository().loadAffiliateTags()) {
|
||||
_tagMap.put(sub.tag, sub.tagId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the gameblast aux data repository.
|
||||
*/
|
||||
public GameBlastAuxRepository getBlastRepository ()
|
||||
{
|
||||
return _blastRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id to which the specified tag has been mapped, assigning a new tag id if
|
||||
* necessary.
|
||||
*
|
||||
* @return -1 if an error occurred assigning a new id.
|
||||
*/
|
||||
public int getAffiliateTagId (String tag)
|
||||
{
|
||||
// if we've already mapped this value, we're good to go
|
||||
Integer tagId = _tagMap.get(tag);
|
||||
if (tagId != null) {
|
||||
return tagId;
|
||||
}
|
||||
|
||||
// register a new affiliate tag and add it to our mappings
|
||||
try {
|
||||
tagId = getRepository().registerAffiliateTag(tag);
|
||||
_tagMap.put(tag, tagId);
|
||||
return tagId;
|
||||
|
||||
} catch (PersistenceException pe) {
|
||||
log.warning("Failed to register new affiliate tag '" + tag + "'.", pe);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Both tag strings and ids are unique, so grab the string (key) based on the id (value)
|
||||
* @param tagId ID to search for (eg 45)
|
||||
* @return String value of the tag or tags (eg "nov07GroupA::DF34A9")
|
||||
*/
|
||||
public String getAffiliateTagString (int tagId)
|
||||
{
|
||||
Integer theTagId = Integer.valueOf(tagId);
|
||||
|
||||
for (Map.Entry<String,Integer> entry : _tagMap.entrySet()) {
|
||||
if (theTagId.equals(entry.getValue())) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the standard {@link #requireUser(HttpServletRequest)} with
|
||||
* the additional requirement that the user hold the specified token.
|
||||
* If they do not, they will be redirected to a page informing them
|
||||
* access is denied.
|
||||
*
|
||||
* @return the user associated with the request.
|
||||
*/
|
||||
public User requireUser (HttpServletRequest req, byte token)
|
||||
throws PersistenceException, RedirectException
|
||||
{
|
||||
OOOUser user = (OOOUser)requireUser(req);
|
||||
if (!user.holdsToken(token)) {
|
||||
throw new RedirectException(_accessDeniedURL);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the standard {@link #requireUser(HttpServletRequest)} with
|
||||
* the additional requirement that the user hold one of the specified
|
||||
* tokens. If they do not, they will be redirected to a page informing
|
||||
* them access is denied.
|
||||
*
|
||||
* @return the user associated with the request.
|
||||
*/
|
||||
public User requireUser (HttpServletRequest req, byte[] tokens)
|
||||
throws PersistenceException, RedirectException
|
||||
{
|
||||
OOOUser user = (OOOUser)requireUser(req);
|
||||
if (!user.holdsAnyToken(tokens)) {
|
||||
throw new RedirectException(_accessDeniedURL);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserRepository createRepository (ConnectionProvider conprov)
|
||||
throws PersistenceException
|
||||
{
|
||||
return new OOOUserRepository(conprov);
|
||||
}
|
||||
|
||||
/** The repository for gameblast auxiliary data. */
|
||||
protected GameBlastAuxRepository _blastRepo;
|
||||
|
||||
/** The URL to which we redirect users whose access is denied. */
|
||||
protected String _accessDeniedURL;
|
||||
|
||||
/** Maintains a mapping of affiliate tags. */
|
||||
protected Map<String,Integer> _tagMap = Maps.newHashMap();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
/**
|
||||
* Defines an XML-RPC interface to certain ooouser services. This is implemented by the register
|
||||
* webapp, but can be implemented by other systems that wish to provide API compatibility with a
|
||||
* potentially different backend implementation.
|
||||
*/
|
||||
public interface OOOXmlRpcService
|
||||
{
|
||||
/**
|
||||
* Returns true if the supplied credentials are valid, false otherwise.
|
||||
*
|
||||
* @param username the name of the user in question.
|
||||
* @param password the MD5 encoded password for the user.
|
||||
*/
|
||||
boolean authUser (String username, String password);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
/**
|
||||
* Records information on a particular referral.
|
||||
*/
|
||||
public class ReferralRecord
|
||||
{
|
||||
/** A unique identifier for this referral record. */
|
||||
public int referralId;
|
||||
|
||||
/** The date on which the referral was made (used to expire old
|
||||
* unutilized referrals). */
|
||||
public Date recorded;
|
||||
|
||||
/** The OOO user id of the referring user. */
|
||||
public int referrerId;
|
||||
|
||||
/** Data associated with this referral record. */
|
||||
public String data;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.JORARepository;
|
||||
import com.samskivert.jdbc.jora.Table;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Tracks the data needed for our personal referral system whereby one
|
||||
* user sends a referral link (or email) to their friend and we track the
|
||||
* original referring user if their referred friend actually registers
|
||||
* within some reasonable time frame (like a couple of weeks).
|
||||
*/
|
||||
public class ReferralRepository extends JORARepository
|
||||
{
|
||||
/**
|
||||
* The database identifier used when establishing a database
|
||||
* connection. This value being <code>referraldb</code>.
|
||||
*/
|
||||
public static final String REFERRAL_DB_IDENT = "referraldb";
|
||||
|
||||
/**
|
||||
* Creates the repository and prepares it for operation.
|
||||
*
|
||||
* @param provider the database connection provider.
|
||||
*/
|
||||
public ReferralRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, REFERRAL_DB_IDENT);
|
||||
|
||||
// figure out whether or not the referral system is in use
|
||||
execute(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
_active = JDBCUtil.tableExists(conn, "REFERRAL");
|
||||
if (!_active) {
|
||||
log.info("No referral table. Disabling.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a new referral record in the system and returns the unique
|
||||
* identifier for said record.
|
||||
*/
|
||||
public int recordReferral (int referrerId, String data)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
throw new PersistenceException("Referral repository is not available.");
|
||||
}
|
||||
|
||||
final ReferralRecord record = new ReferralRecord();
|
||||
record.recorded = new Date(System.currentTimeMillis());
|
||||
record.referrerId = referrerId;
|
||||
record.data = data;
|
||||
record.referralId = insert(_rtable, record);
|
||||
|
||||
checkPurge();
|
||||
return record.referralId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a referral record with the specified id, returning null if
|
||||
* no matching record could be found.
|
||||
*/
|
||||
public ReferralRecord lookupReferral (int referralId)
|
||||
throws PersistenceException
|
||||
{
|
||||
return lookupReferralBy("where REFERRAL_ID = " + referralId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a referral record with the specified referring user id,
|
||||
* returning null if no matching record could be found.
|
||||
*/
|
||||
public ReferralRecord lookupReferrer (int referrerId)
|
||||
throws PersistenceException
|
||||
{
|
||||
return lookupReferralBy("where REFERRER_ID = " + referrerId);
|
||||
}
|
||||
|
||||
/** Looks up a referral record matching the specified query. */
|
||||
protected ReferralRecord lookupReferralBy (final String query)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ReferralRecord record = load(_rtable, query);
|
||||
checkPurge();
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to periodically purge old referral records from the
|
||||
* repository.
|
||||
*/
|
||||
protected void checkPurge ()
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - _lastPurge > PURGE_INTERVAL) {
|
||||
_lastPurge = now;
|
||||
try {
|
||||
purgeStaleReferrals();
|
||||
} catch (PersistenceException pe) {
|
||||
log.warning("Error purging referrals: " + pe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges stale referral records from the repository.
|
||||
*/
|
||||
protected void purgeStaleReferrals ()
|
||||
throws PersistenceException
|
||||
{
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
String pquery = "delete from REFERRAL " +
|
||||
"where DATE_SUB(NOW(), INTERVAL 1 MONTH) > RECORDED;";
|
||||
Statement stmt = conn.createStatement();
|
||||
try {
|
||||
stmt.executeUpdate(pquery);
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTables ()
|
||||
{
|
||||
_rtable = new Table<ReferralRecord>(ReferralRecord.class, "REFERRAL", "REFERRAL_ID", true);
|
||||
}
|
||||
|
||||
/** The table used to new billing actions that have taken place. */
|
||||
protected Table<ReferralRecord> _rtable;
|
||||
|
||||
/** If we don't detect our tables, we deactivate ourselves. */
|
||||
protected boolean _active;
|
||||
|
||||
/** The last time we purged the repository of stale records. */
|
||||
protected long _lastPurge;
|
||||
|
||||
/** We purge the repository every three hours of stale records. */
|
||||
protected static final long PURGE_INTERVAL = 3 * 60 * 60 * 1000L;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
/*
|
||||
* Copyright (C) 2007-2010 Three Rings Design, Inc.
|
||||
* Copyright 1999,2004 The Apache Software Foundation.
|
||||
*/
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.StaticConnectionProvider;
|
||||
import com.samskivert.servlet.JDBCTableSiteIdentifier;
|
||||
import com.samskivert.servlet.SiteIdentifier;
|
||||
import com.samskivert.util.Config;
|
||||
|
||||
/**
|
||||
* Encapsulates multiple repositories, and offers public access to all of them.
|
||||
*/
|
||||
public class RepositoryManager {
|
||||
|
||||
/** The repo manager will be stored in context under this attribute name */
|
||||
public static final String ATTRIBUTE_NAME = RepositoryManager.class.getName();
|
||||
|
||||
/** Our User Manager */
|
||||
public OOOUserManager userRepository;
|
||||
|
||||
/** Our referral repository. */
|
||||
public ReferralRepository referralRepository;
|
||||
|
||||
/** Our site identification repository. */
|
||||
public SiteIdentifier siteRepository;
|
||||
|
||||
/** For logging tracking events */
|
||||
public TrackingRepository trackingRepository;
|
||||
|
||||
/**
|
||||
* Initialize a new set of repositories.
|
||||
* @param config
|
||||
*/
|
||||
public RepositoryManager (Config config)
|
||||
throws PersistenceException
|
||||
{
|
||||
// create a static connection provider
|
||||
StaticConnectionProvider conn = new StaticConnectionProvider(config.getSubProperties("db"));
|
||||
|
||||
/* create the referral repository */
|
||||
referralRepository = new ReferralRepository(conn);
|
||||
|
||||
/* create a repository of users */
|
||||
userRepository = new OOOUserManager(config.getSubProperties("oooauth"), conn);
|
||||
|
||||
/* create a repository of sites */
|
||||
siteRepository = new JDBCTableSiteIdentifier(conn);
|
||||
|
||||
/* create a repository for tracking */
|
||||
trackingRepository = new TrackingRepository(conn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* An object representation of the persistent reward info stored in the {@link RewardRepository}
|
||||
* REWARD_INFO table.
|
||||
*/
|
||||
public class RewardInfo
|
||||
{
|
||||
/** Unique reward id. */
|
||||
public int rewardId;
|
||||
|
||||
/** Reward description. */
|
||||
public String description;
|
||||
|
||||
/** A coupon code required to activate this reward or null. */
|
||||
public String couponCode;
|
||||
|
||||
/** Game specific reward data. */
|
||||
public String data;
|
||||
|
||||
/** Date when the reward expires. */
|
||||
public Date expiration;
|
||||
|
||||
/** Max userid that can redeem this reward. */
|
||||
public int maxEligibleId;
|
||||
|
||||
/** Number of activated rewards. */
|
||||
public int activations;
|
||||
|
||||
/** Number of claimed rewards. */
|
||||
public int redemptions;
|
||||
|
||||
@Override // from Object
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* An object representation of the persistent rewards stored in the {@link RewardRepository} REWARDS
|
||||
* table.
|
||||
*/
|
||||
public class RewardRecord
|
||||
{
|
||||
/** RewardInfo reward id. */
|
||||
public int rewardId;
|
||||
|
||||
/** Account name. */
|
||||
public String account;
|
||||
|
||||
/** Unique identification value. */
|
||||
public String redeemerIdent;
|
||||
|
||||
/**
|
||||
* A generic parameter string that can tell the reward handler more specifically when and what
|
||||
* item, etc. to hand out for this instance of the reward.
|
||||
*/
|
||||
public String param;
|
||||
|
||||
/**
|
||||
* Constructs a blank RewardRecord for unserialization from the repository.
|
||||
*/
|
||||
public RewardRecord ()
|
||||
{
|
||||
}
|
||||
|
||||
@Override // from Object
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.JORARepository;
|
||||
import com.samskivert.jdbc.jora.Cursor;
|
||||
import com.samskivert.jdbc.jora.FieldMask;
|
||||
import com.samskivert.jdbc.jora.Table;
|
||||
|
||||
/**
|
||||
* Maintains persistent reward information.
|
||||
*/
|
||||
public class RewardRepository extends JORARepository
|
||||
{
|
||||
/**
|
||||
* Creates the repository with the specified connection provider.
|
||||
*
|
||||
* @exception PersistenceException thrown if an error occurs communicating with the underlying
|
||||
* persistence facilities.
|
||||
*/
|
||||
public RewardRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, OOOUserRepository.USER_REPOSITORY_IDENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RewardInfo record in the database. <code>info</code> should have the
|
||||
* <code>description</code>, <code>data</code> and <code>expiration</code> filled in.
|
||||
*/
|
||||
public void createReward (RewardInfo info)
|
||||
throws PersistenceException
|
||||
{
|
||||
info.maxEligibleId = getMaxUserId();
|
||||
store(_infoTable, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately expires a reward by setting the expiration to the current date then making a
|
||||
* call to <code>purgeExpiredRewards</code>.
|
||||
*/
|
||||
public void expireReward (int rewardId)
|
||||
throws PersistenceException
|
||||
{
|
||||
RewardInfo info = new RewardInfo();
|
||||
info.rewardId = rewardId;
|
||||
info.expiration = new Date(System.currentTimeMillis());
|
||||
updateField(_infoTable, info, "expiration");
|
||||
purgeExpiredRewards();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to activate the reward for an account. If this account has not already activated
|
||||
* the reward, a new RewardRecord will be created. Returns true if a new RewardRecord is
|
||||
* created, false otherwise.
|
||||
*/
|
||||
public boolean activateReward (int rewardId, String account)
|
||||
throws PersistenceException
|
||||
{
|
||||
return activateReward(rewardId, account, null);
|
||||
}
|
||||
|
||||
public boolean activateReward (int rewardId, String account, String param)
|
||||
throws PersistenceException
|
||||
{
|
||||
final RewardRecord rr = new RewardRecord();
|
||||
rr.rewardId = rewardId;
|
||||
rr.account = account;
|
||||
rr.param = param == null ? "" : param;
|
||||
// try to insert it; catching duplicate row exceptions and reporting that the reward is
|
||||
// already activated
|
||||
return executeUpdate(new Operation<Boolean>() {
|
||||
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
try {
|
||||
_rewardsTable.insert(conn, rr);
|
||||
return true;
|
||||
} catch (SQLException sqe) {
|
||||
if (liaison.isDuplicateRowException(sqe)) {
|
||||
return false;
|
||||
} else {
|
||||
throw sqe;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates monthly rewards of the specified ID for the account between start and end. If they
|
||||
* already have monthly rewards for this ID, will add new ones only at appropriate monthly
|
||||
* intervals from the existing ones.
|
||||
*/
|
||||
public boolean activateMonthlyRewards (final int rewardId, final String account,
|
||||
final java.util.Date start, final java.util.Date end)
|
||||
throws PersistenceException
|
||||
{
|
||||
// Setup an example to find relevant rewards.
|
||||
final RewardRecord example = new RewardRecord();
|
||||
example.rewardId = rewardId;
|
||||
example.account = account;
|
||||
final FieldMask mask = _rewardsTable.getFieldMask();
|
||||
mask.setModified("rewardId");
|
||||
mask.setModified("account");
|
||||
|
||||
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
return executeUpdate(new Operation<Boolean>() {
|
||||
public Boolean invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
// Find the most recent reward this account already has of this type.
|
||||
java.util.Date latest = null;
|
||||
Cursor<RewardRecord> matches =
|
||||
_rewardsTable.queryByExample(conn, example, mask);
|
||||
RewardRecord rec;
|
||||
while ((rec = matches.next()) != null) {
|
||||
if (rec.param == null) {
|
||||
log.warning("Missing monthly reward param: ", "rewardId", rewardId,
|
||||
"account", account);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
java.util.Date thisDate = dateFormat.parse(rec.param);
|
||||
if (latest == null || thisDate.after(latest)) {
|
||||
latest = thisDate;
|
||||
}
|
||||
} catch (ParseException pe) {
|
||||
log.warning("Bogus reward param: ", "rewardId", rewardId,
|
||||
"account", account, "param", rec.param);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// We use the maximum of our provided start time and a month-after-latest
|
||||
// as the first time we'll input a reward. Note that this can result
|
||||
// in no rewards being given.
|
||||
Calendar startCal = Calendar.getInstance();
|
||||
if (latest != null) {
|
||||
startCal.setTime(latest);
|
||||
startCal.add(Calendar.MONTH, 1);
|
||||
if (startCal.getTime().before(start)) {
|
||||
startCal.setTime(start);
|
||||
}
|
||||
} else {
|
||||
startCal.setTime(start);
|
||||
}
|
||||
|
||||
// Add in any appropriate rewards between the modified start and end.
|
||||
while (startCal.getTime().before(end)) {
|
||||
final RewardRecord rr = new RewardRecord();
|
||||
rr.rewardId = rewardId;
|
||||
rr.account = account;
|
||||
rr.param = dateFormat.format(startCal.getTime());
|
||||
_rewardsTable.insert(conn, rr);
|
||||
startCal.add(Calendar.MONTH, 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates all rewards of the specified ID and account during the time window.
|
||||
* Returns the number of rewards deleted.
|
||||
*/
|
||||
public int deactivateMonthlyRewards (final int rewardId, final String account,
|
||||
final java.util.Date start, final java.util.Date end)
|
||||
throws PersistenceException
|
||||
{
|
||||
return executeUpdate(new Operation<Integer>() {
|
||||
public Integer invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
String delQuery = "delete from REWARD_RECORDS where REWARD_ID = ? and ACCOUNT = ?" +
|
||||
" and PARAM >= ? and PARAM <= ? and REDEEMER_IDENT is null";
|
||||
|
||||
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
String startStr = dateFormat.format(start);
|
||||
String endStr = dateFormat.format(end);
|
||||
|
||||
PreparedStatement stmt = null;
|
||||
try {
|
||||
stmt = conn.prepareStatement(delQuery);
|
||||
stmt.setInt(1, rewardId);
|
||||
stmt.setString(2, account);
|
||||
stmt.setString(3, startStr);
|
||||
stmt.setString(4, endStr);
|
||||
return stmt.executeUpdate();
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all currently active rewards.
|
||||
*/
|
||||
public List<RewardInfo> loadActiveRewards ()
|
||||
throws PersistenceException
|
||||
{
|
||||
return loadAll(_infoTable, "where EXPIRATION > NOW()");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all rewards.
|
||||
*/
|
||||
public List<RewardInfo> loadRewards ()
|
||||
throws PersistenceException
|
||||
{
|
||||
return loadAll(_infoTable, "order by REWARD_ID desc");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s that match the given account name and whose eligible
|
||||
* date is in the past. The returned list will be sorted by <code>rewardId</code>.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedRewards (String account)
|
||||
throws PersistenceException
|
||||
{
|
||||
String condition = "where ACCOUNT = " + JDBCUtil.escape(account) +
|
||||
" order by REWARD_ID";
|
||||
return loadAll(_rewardsTable, condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s that match either the account or redeemer identifier.
|
||||
* The returned list will be sorted by <code>rewardId</code>.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedRewards (String account, String redeemerIdent)
|
||||
throws PersistenceException
|
||||
{
|
||||
String condition = "where ACCOUNT = " + JDBCUtil.escape(account) +
|
||||
" or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) +
|
||||
" order by REWARD_ID, PARAM";
|
||||
return loadAll(_rewardsTable, condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> that match
|
||||
* either the account or redeemer identifier.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedReward (
|
||||
String account, String redeemerIdent, int rewardId)
|
||||
throws PersistenceException
|
||||
{
|
||||
String condition = "where REWARD_ID = " + rewardId +
|
||||
" and (ACCOUNT = " + JDBCUtil.escape(account) +
|
||||
" or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + ")";
|
||||
return loadAll(_rewardsTable, condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> and
|
||||
* <code>param</code> that match either the account or redeemer identifier.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedReward (
|
||||
String account, String redeemerIdent, int rewardId, String param)
|
||||
throws PersistenceException
|
||||
{
|
||||
String paramStr = (param == null) ? "PARAM is NULL" : "PARAM = " + JDBCUtil.escape(param);
|
||||
String condition = "where REWARD_ID = " + rewardId +
|
||||
" and " + paramStr +
|
||||
" and (ACCOUNT = " + JDBCUtil.escape(account) +
|
||||
" or REDEEMER_IDENT = " + JDBCUtil.escape(redeemerIdent) + ")";
|
||||
return loadAll(_rewardsTable, condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the supplied RewardRecord with the indicated <code>redeemerIdent</code> and store it
|
||||
* to the database.
|
||||
*/
|
||||
public boolean redeemReward (RewardRecord record, String redeemerIdent)
|
||||
throws PersistenceException
|
||||
{
|
||||
String update = "update " + _rewardsTable.getName() + " set REDEEMER_IDENT = " +
|
||||
JDBCUtil.escape(redeemerIdent) + " where REDEEMER_IDENT is NULL and REWARD_ID = " +
|
||||
record.rewardId + " and ACCOUNT = " + JDBCUtil.escape(record.account) +
|
||||
" and PARAM = " + JDBCUtil.escape(record.param);
|
||||
return update(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarizes the reward data and purges expired rewards.
|
||||
*/
|
||||
public void purgeExpiredRewards ()
|
||||
throws PersistenceException
|
||||
{
|
||||
executeUpdate(new Operation<Object> () {
|
||||
public Object invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
summarizeAndUpdate(conn, "", "ACTIVATIONS");
|
||||
summarizeAndUpdate(conn, "and REDEEMER_IDENT is NOT NULL", "REDEMPTIONS");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
update("delete from REWARD_RECORDS using REWARD_RECORDS, REWARD_INFO " +
|
||||
"where REWARD_INFO.REWARD_ID = REWARD_RECORDS.REWARD_ID " +
|
||||
"and REWARD_INFO.EXPIRATION <= NOW()");
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarizes the reward data and stores it in the reward info records.
|
||||
*/
|
||||
protected void summarizeAndUpdate (Connection conn, String condition, String column)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
String activatedQuery = "select count(*), REWARD_INFO.REWARD_ID " +
|
||||
"from REWARD_RECORDS, REWARD_INFO " +
|
||||
"where REWARD_INFO.REWARD_ID = REWARD_RECORDS.REWARD_ID " + condition +
|
||||
" group by REWARD_ID";
|
||||
String activatedUpdate = "update REWARD_INFO set " + column + " = ? where REWARD_ID = ?";
|
||||
PreparedStatement ustmt = null;
|
||||
Statement stmt = null;
|
||||
try {
|
||||
stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(activatedQuery);
|
||||
ustmt = conn.prepareStatement(activatedUpdate);
|
||||
while (rs.next()) {
|
||||
ustmt.setInt(1, rs.getInt(1));
|
||||
ustmt.setInt(2, rs.getInt(2));
|
||||
ustmt.executeUpdate();
|
||||
}
|
||||
} finally {
|
||||
JDBCUtil.close(ustmt);
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the max userid currently in use.
|
||||
*/
|
||||
protected int getMaxUserId ()
|
||||
throws PersistenceException
|
||||
{
|
||||
return execute(new Operation<Integer> () {
|
||||
public Integer invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
String query = "select MAX(userId) from users";
|
||||
Statement stmt = null;
|
||||
int maxUserId = 0;
|
||||
|
||||
try {
|
||||
stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(query);
|
||||
if (rs.next()) {
|
||||
maxUserId = rs.getInt(1);
|
||||
} else {
|
||||
log.warning("No users found!?");
|
||||
}
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
|
||||
return maxUserId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
String[] rewardInfoTable = {
|
||||
"REWARD_ID INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY",
|
||||
"DESCRIPTION VARCHAR(255) NOT NULL",
|
||||
"COUPON_CODE VARCHAR(255)",
|
||||
"DATA VARCHAR(255) NOT NULL",
|
||||
"EXPIRATION DATE NOT NULL",
|
||||
"MAX_ELIGIBLE_ID INTEGER NOT NULL",
|
||||
"ACTIVATIONS INTEGER NOT NULL",
|
||||
"REDEMPTIONS INTEGER NOT NULL"
|
||||
};
|
||||
JDBCUtil.createTableIfMissing(conn, "REWARD_INFO", rewardInfoTable, "");
|
||||
|
||||
// TEMP: (2006-12-22) add COUPON_CODE column
|
||||
JDBCUtil.addColumn(conn, "REWARD_INFO", "COUPON_CODE", "VARCHAR(255)", "DESCRIPTION");
|
||||
// END TEMP
|
||||
|
||||
String[] rewardsTable = {
|
||||
"REWARD_ID INTEGER NOT NULL",
|
||||
"ACCOUNT VARCHAR(255)",
|
||||
"REDEEMER_IDENT VARCHAR(64)",
|
||||
"PARAM VARCHAR(255)",
|
||||
"PRIMARY KEY (REWARD_ID, ACCOUNT, PARAM)",
|
||||
"INDEX (ACCOUNT)",
|
||||
"INDEX (REDEEMER_IDENT)"
|
||||
};
|
||||
JDBCUtil.createTableIfMissing(conn, "REWARD_RECORDS", rewardsTable, "");
|
||||
|
||||
// Add a parameter column. This allows a particular reward to be granted more than once, and
|
||||
// pass specific details to the reward handler.
|
||||
if (!JDBCUtil.tableContainsColumn(conn, "REWARD_RECORDS", "PARAM")) {
|
||||
// unfortunately, we need to recreate the primary key
|
||||
JDBCUtil.dropPrimaryKey(conn, "REWARD_RECORDS");
|
||||
|
||||
// add generic parameter column, and include
|
||||
JDBCUtil.addColumn(conn, "REWARD_RECORDS", "PARAM", "VARCHAR(255), " +
|
||||
"ADD PRIMARY KEY (REWARD_ID, ACCOUNT, PARAM)", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTables ()
|
||||
{
|
||||
_infoTable = new Table<RewardInfo>(
|
||||
RewardInfo.class, "REWARD_INFO", REWARD_INFO_PRIMARY_KEY, true);
|
||||
_rewardsTable = new Table<RewardRecord>(
|
||||
RewardRecord.class, "REWARD_RECORDS", REWARD_RECORDS_PRIMARY_KEYS, true);
|
||||
}
|
||||
|
||||
/** The primary key for the reward info table. */
|
||||
protected static final String REWARD_INFO_PRIMARY_KEY = "REWARD_ID";
|
||||
|
||||
/** The primary keys for the rewards table. */
|
||||
protected static final String[] REWARD_RECORDS_PRIMARY_KEYS = {
|
||||
"REWARD_ID", "ACCOUNT", "PARAM"
|
||||
};
|
||||
|
||||
/** The table used to store reward information. */
|
||||
protected Table<RewardInfo> _infoTable;
|
||||
|
||||
/** The table used to store activated rewards. */
|
||||
protected Table<RewardRecord> _rewardsTable;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
/**
|
||||
* Represents a row in the TAINTED_IDENTS table.
|
||||
*/
|
||||
public class TaintedIdent
|
||||
{
|
||||
/** A 'unique' id for a specific machine we have seen. */
|
||||
public String machIdent;
|
||||
|
||||
/** Blank constructor for the unserialization buisness. */
|
||||
public TaintedIdent ()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return machIdent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.PreparedStatement;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.SimpleRepository;
|
||||
|
||||
/**
|
||||
* Manages database access for doing our own quick and dirty tracking ala clientstep.
|
||||
*/
|
||||
public class TrackingRepository extends SimpleRepository
|
||||
{
|
||||
/** Name of the tracking table. */
|
||||
public static final String TRACKING_TABLE = "TRACKING";
|
||||
|
||||
/**
|
||||
* The database identifier used when establishing a database
|
||||
* connection. This value being <code>trackingdb</code>.
|
||||
*/
|
||||
public static final String TRACKING_DB_IDENT = "trackingdb";
|
||||
|
||||
/**
|
||||
* Creates the repository and prepares it for operation.
|
||||
*
|
||||
* @param provider the database connection provider.
|
||||
*/
|
||||
public TrackingRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, TRACKING_DB_IDENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a tracking event to the database.
|
||||
*/
|
||||
public void addTrackingEvent (final String event, final String description,
|
||||
final int accountId, final int siteId)
|
||||
throws PersistenceException
|
||||
{
|
||||
executeUpdate(new Operation<Object>() {
|
||||
public Object invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
PreparedStatement stmt = null;
|
||||
try {
|
||||
stmt = conn.prepareStatement("insert into " + TRACKING_TABLE +
|
||||
" (TIME, EVENT, DESCRIPTION, ACCOUNT_ID, SITE_ID)" +
|
||||
" values (NOW(), ?, ?, ?, ?)");
|
||||
stmt.setString(1, event);
|
||||
stmt.setString(2, description);
|
||||
stmt.setInt(3, accountId);
|
||||
stmt.setInt(4, siteId);
|
||||
|
||||
JDBCUtil.checkedUpdate(stmt, 1);
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
String[] TrackingTable = {
|
||||
"TIME DATETIME NOT NULL",
|
||||
"EVENT VARCHAR(255)",
|
||||
"DESCRIPTION VARCHAR(255)",
|
||||
"ACCOUNT_ID INTEGER",
|
||||
"SITE_ID INTEGER",
|
||||
};
|
||||
JDBCUtil.createTableIfMissing(conn, TRACKING_TABLE, TrackingTable, "");
|
||||
|
||||
if (!JDBCUtil.tableContainsColumn(conn, TRACKING_TABLE, "ACCOUNT_ID")) {
|
||||
JDBCUtil.addColumn(conn, TRACKING_TABLE, "ACCOUNT_ID", "INTEGER", "DESCRIPTION");
|
||||
JDBCUtil.addColumn(conn, TRACKING_TABLE, "SITE_ID", "INTEGER", "ACCOUNT_ID");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.samskivert.net.MailUtil;
|
||||
import com.samskivert.servlet.util.DataValidationException;
|
||||
import com.samskivert.servlet.util.ParameterUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Miscellaneous user data related routines.
|
||||
*/
|
||||
public class UserDataUtil
|
||||
{
|
||||
/** The minimum length of a valid password. */
|
||||
public static final int MIN_PASSWORD_LENGTH = 4;
|
||||
|
||||
/** The minimum length of a valid username. */
|
||||
public static final int MIN_USERNAME_LENGTH = 3;
|
||||
|
||||
/** The maximum length of a valid username. */
|
||||
public static final int MAX_USERNAME_LENGTH = 12;
|
||||
|
||||
/**
|
||||
* Returns the username in the supplied request, or throws an
|
||||
* informative exception if the username is absent or invalid.
|
||||
*/
|
||||
public static String getUsername (HttpServletRequest req, String page)
|
||||
throws DataValidationException
|
||||
{
|
||||
String username = ParameterUtil.requireParameter(
|
||||
req, "username", page + ".error.missing_username");
|
||||
username = username.trim();
|
||||
int len = username.length();
|
||||
if (len < MIN_USERNAME_LENGTH || len > MAX_USERNAME_LENGTH ||
|
||||
!username.matches("[a-zA-Z0-9_]+")) {
|
||||
throw new DataValidationException(page + ".error.invalid_username");
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password in the supplied request, or throws an
|
||||
* informative exception if the password is absent or invalid.
|
||||
*/
|
||||
public static String getPassword (
|
||||
HttpServletRequest req, String page, String ptype)
|
||||
throws DataValidationException
|
||||
{
|
||||
return getPassword(req, page, ptype, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password in the supplied request, or throws an
|
||||
* informative exception if the password is absent or invalid.
|
||||
*
|
||||
* @optional If true, null may be returned and if the parameter is absent no exception will be
|
||||
* thrown. If the parameter is present it is still checked for validity.
|
||||
*/
|
||||
public static String getPassword (
|
||||
HttpServletRequest req, String page, String ptype, boolean optional)
|
||||
throws DataValidationException
|
||||
{
|
||||
String password = optional ? ParameterUtil.getParameter(req, ptype, true) :
|
||||
ParameterUtil.requireParameter(req, ptype, page + ".error.missing_" + ptype);
|
||||
if (password != null) {
|
||||
checkPassword(page, password, ptype);
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an informative exception if the password is absent or
|
||||
* invalid.
|
||||
*/
|
||||
public static void checkPassword (
|
||||
String page, String password, String ptype)
|
||||
throws DataValidationException
|
||||
{
|
||||
int len = password.length();
|
||||
if (len < MIN_PASSWORD_LENGTH) {
|
||||
throw new DataValidationException(
|
||||
page + ".error.invalid_" + ptype);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email in the supplied request, or throws an informative
|
||||
* exception if the email is absent or invalid.
|
||||
*/
|
||||
public static String getEmail (
|
||||
HttpServletRequest req, String page, boolean require)
|
||||
throws DataValidationException
|
||||
{
|
||||
String email = ParameterUtil.getParameter(req, "email", true);
|
||||
if (StringUtil.isBlank(email)) {
|
||||
if (require) {
|
||||
throw new DataValidationException(
|
||||
page + ".error.missing_email");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
email = email.trim();
|
||||
if (email.length() > MAX_EMAIL_LENGTH ||
|
||||
!MailUtil.isValidAddress(email)) {
|
||||
throw new DataValidationException(page + ".error.invalid_email");
|
||||
}
|
||||
return email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified partial (first or last) name in the supplied
|
||||
* request, or throws an informative exception if the name is absent
|
||||
* or invalid.
|
||||
*/
|
||||
public static String getName (
|
||||
HttpServletRequest req, String page, String pname)
|
||||
throws DataValidationException
|
||||
{
|
||||
String name = ParameterUtil.requireParameter(
|
||||
req, pname, page + ".error.missing_" + pname).trim();
|
||||
int len = name.length();
|
||||
if (len < MIN_NAME_LENGTH || len > MAX_NAME_LENGTH ||
|
||||
!name.matches("[a-zA-Z]+")) {
|
||||
throw new DataValidationException(
|
||||
page + ".error.invalid_" + pname);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state in the supplied request, or throws an informative
|
||||
* exception if the state is absent or invalid.
|
||||
*/
|
||||
public static String getState (HttpServletRequest req, String page)
|
||||
throws DataValidationException
|
||||
{
|
||||
String state = ParameterUtil.getParameter(req, "state", false);
|
||||
state = state.trim();
|
||||
if (state.length() > MAX_STATE_LENGTH) {
|
||||
throw new DataValidationException(
|
||||
page + ".error.invalid_state");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the country in the supplied request, or throws an
|
||||
* informative exception if the country is absent or invalid.
|
||||
*/
|
||||
public static String getCountry (HttpServletRequest req, String page)
|
||||
throws DataValidationException
|
||||
{
|
||||
String country = ParameterUtil.requireParameter(
|
||||
req, "country", page + ".error.missing_country");
|
||||
country = country.trim();
|
||||
int len = country.length();
|
||||
if (len < MIN_COUNTRY_LENGTH || len > MAX_COUNTRY_LENGTH) {
|
||||
throw new DataValidationException(
|
||||
page + ".error.invalid_country");
|
||||
}
|
||||
return country;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the missive in the supplied request, or throws an
|
||||
* informative exception if the missive is absent or invalid.
|
||||
*/
|
||||
public static String getMissive (HttpServletRequest req, String page)
|
||||
throws DataValidationException
|
||||
{
|
||||
String missive = ParameterUtil.getParameter(req, "missive", false);
|
||||
missive = missive.trim();
|
||||
if (missive.length() > MAX_MISSIVE_LENGTH) {
|
||||
throw new DataValidationException(
|
||||
page + ".error.invalid_missive");
|
||||
}
|
||||
return missive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 1 if the named parameter is present in the supplied
|
||||
* request, 0 if not.
|
||||
*/
|
||||
public static byte isChecked (HttpServletRequest req, String name)
|
||||
throws DataValidationException
|
||||
{
|
||||
String value = ParameterUtil.getParameter(req, name, true);
|
||||
return (byte)((value != null) ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the given select menu, or throws an error if
|
||||
* the value is not defined or 0 (denoting the default "[Select One]"
|
||||
* selection.)
|
||||
*/
|
||||
public static byte requireSelectItem (
|
||||
HttpServletRequest req, String name, String error)
|
||||
throws DataValidationException
|
||||
{
|
||||
byte value = (byte)ParameterUtil.getIntParameter(req, name, 0, error);
|
||||
if (value == 0) {
|
||||
throw new DataValidationException(error);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** The maximum length of a valid email address. */
|
||||
protected static final int MAX_EMAIL_LENGTH = 128;
|
||||
|
||||
/** The minimum length of a valid name in characters. */
|
||||
protected static final int MIN_NAME_LENGTH = 1;
|
||||
|
||||
/** The maximum length of a valid name in characters. */
|
||||
protected static final int MAX_NAME_LENGTH = 63;
|
||||
|
||||
/** The minimum birth year a user can enter. God help us if we have
|
||||
* many centegenarians. */
|
||||
protected static final int MIN_BIRTHYEAR = 1900;
|
||||
|
||||
/** The minimum birth date a user can enter. */
|
||||
protected static final int MIN_BIRTHDATE = 1;
|
||||
|
||||
/** The maximum birth date a user can enter. */
|
||||
protected static final int MAX_BIRTHDATE = 31;
|
||||
|
||||
/** The minimum length of a valid country in characters. */
|
||||
protected static final int MIN_COUNTRY_LENGTH = 2;
|
||||
|
||||
/** The maximum length of a valid state in characters. */
|
||||
protected static final int MAX_STATE_LENGTH = 64;
|
||||
|
||||
/** The maximum length of a valid country in characters. */
|
||||
protected static final int MAX_COUNTRY_LENGTH = 64;
|
||||
|
||||
/** The maximum length of a valid missive in characters. */
|
||||
protected static final int MAX_MISSIVE_LENGTH = 32768;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
/**
|
||||
* Represents a row in the USER_IDENT table mapping a user to all their
|
||||
* machine identifieres.
|
||||
*/
|
||||
public class UserIdent
|
||||
{
|
||||
/** The id of the user in question. */
|
||||
public int userId;
|
||||
|
||||
/** A 'unique' id for a specific machine we have seen the user come from. */
|
||||
public String machIdent;
|
||||
|
||||
/** Construct a blank record for unserialization purposes. */
|
||||
public UserIdent ()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return "[id=" + userId + ", ident=" + machIdent + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
/**
|
||||
* Maintains information on users that have not yet validated their
|
||||
* account.
|
||||
*/
|
||||
public class ValidateRecord
|
||||
{
|
||||
public String secret;
|
||||
|
||||
public int userId;
|
||||
|
||||
public boolean persist;
|
||||
|
||||
public Date inserted;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* A record detailing a user's non-critical responses to the Yohoho beta
|
||||
* registration.
|
||||
*/
|
||||
public class YoBetaInfo
|
||||
{
|
||||
/** The user id of the user. */
|
||||
public int userId;
|
||||
|
||||
/** The user's birthday. */
|
||||
public Date birthday;
|
||||
|
||||
/** The user's state or province. */
|
||||
public String state;
|
||||
|
||||
/** The user's country. */
|
||||
public String country;
|
||||
|
||||
/** The user's connection speed. */
|
||||
public byte connection;
|
||||
|
||||
/** The user's computer operating system. */
|
||||
public byte os;
|
||||
|
||||
/** The user's computer CPU speed. */
|
||||
public byte cpu;
|
||||
|
||||
/** The user's computer memory size. */
|
||||
public byte memory;
|
||||
|
||||
/** Whether the user has played an MMORPG. */
|
||||
public byte playMmorpg;
|
||||
|
||||
/** Whether the user has played a beta MMORPG. */
|
||||
public byte playBetaMmorpg;
|
||||
|
||||
/** Whether the user has played a text MUD. */
|
||||
public byte playMud;
|
||||
|
||||
/** Whether the user plays puzzle games. */
|
||||
public byte playPuzzle;
|
||||
|
||||
/** The user's testing availability this summer. */
|
||||
public byte summer;
|
||||
|
||||
/** The source from which the user heard about Yohoho. */
|
||||
public byte source;
|
||||
|
||||
/** The user's personal missive to us. */
|
||||
public String missive;
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.jdbc.DatabaseLiaison;
|
||||
import com.samskivert.jdbc.JDBCUtil;
|
||||
import com.samskivert.jdbc.JORARepository;
|
||||
import com.samskivert.jdbc.jora.FieldMask;
|
||||
import com.samskivert.jdbc.jora.Table;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Provides facilities for saving a user's Yohoho beta information to the
|
||||
* persistent beta info table in the database.
|
||||
*/
|
||||
public class YoBetaRepository extends JORARepository
|
||||
{
|
||||
/**
|
||||
* The database identifier used when establishing a database
|
||||
* connection. This value being <code>yobetadb</code>.
|
||||
*/
|
||||
public static final String YOBETA_DB_IDENT = "yobetadb";
|
||||
|
||||
/**
|
||||
* Creates the yo beta repository.
|
||||
*
|
||||
* @param provider the database connection provider.
|
||||
*/
|
||||
public YoBetaRepository (ConnectionProvider provider)
|
||||
throws PersistenceException
|
||||
{
|
||||
super(provider, YOBETA_DB_IDENT);
|
||||
|
||||
// figure out whether or not the YOBETA table is in use on this system
|
||||
execute(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws PersistenceException, SQLException
|
||||
{
|
||||
_active = JDBCUtil.tableExists(conn, YOBETA_TABLE_NAME);
|
||||
if (!_active) {
|
||||
log.info("No beta repository table. Disabling.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the supplied yo beta info for the specified user into the
|
||||
* database.
|
||||
*/
|
||||
public void insertYoBetaInfo (final YoBetaInfo ybi)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
return;
|
||||
}
|
||||
insert(_table, ybi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns the yo beta info for the specified user id from
|
||||
* the database, or <code>null</code> if no beta info for the user
|
||||
* exists.
|
||||
*/
|
||||
public YoBetaInfo loadYoBetaInfo (final int userId)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
YoBetaInfo proto = new YoBetaInfo();
|
||||
proto.userId = userId;
|
||||
FieldMask mask = _table.getFieldMask();
|
||||
mask.setModified("userId");
|
||||
return loadByExample(_table, proto, mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the yo beta info for the specified user id from the
|
||||
* database.
|
||||
*/
|
||||
public void deleteYoBetaInfo (final int userId)
|
||||
throws PersistenceException
|
||||
{
|
||||
if (!_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
executeUpdate(new Operation<Void>() {
|
||||
public Void invoke (Connection conn, DatabaseLiaison liaison)
|
||||
throws SQLException, PersistenceException
|
||||
{
|
||||
PreparedStatement stmt = null;
|
||||
try {
|
||||
stmt = conn.prepareStatement(
|
||||
"delete from " + YOBETA_TABLE_NAME + " " +
|
||||
"where USER_ID = ?");
|
||||
stmt.setInt(1, userId);
|
||||
stmt.executeUpdate();
|
||||
|
||||
} finally {
|
||||
JDBCUtil.close(stmt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTables ()
|
||||
{
|
||||
_table = new Table<YoBetaInfo>(
|
||||
YoBetaInfo.class, YOBETA_TABLE_NAME,
|
||||
YOBETA_TABLE_PRIMARY_KEY, true);
|
||||
}
|
||||
|
||||
/** A wrapper that provides access to the yo beta table. */
|
||||
protected Table<YoBetaInfo> _table;
|
||||
|
||||
/** Whether or not we are actually in use. If our table doesn't exist,
|
||||
* we quietly disable ourselves. */
|
||||
protected boolean _active;
|
||||
|
||||
/** The name of the yobeta table. */
|
||||
protected static final String YOBETA_TABLE_NAME = "YOBETA";
|
||||
|
||||
/** The primary key for the yobeta table. */
|
||||
protected static final String YOBETA_TABLE_PRIMARY_KEY = "USER_ID";
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.GenerationType;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
import com.threerings.user.AccountAction;
|
||||
|
||||
/**
|
||||
* Emulates {@link AccountAction} for the Depot.
|
||||
*/
|
||||
@Entity(name="ACCOUNT_ACTIONS")
|
||||
public class AccountActionRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<AccountActionRecord> _R = AccountActionRecord.class;
|
||||
public static final ColumnExp<Integer> ACTION_ID = colexp(_R, "actionId");
|
||||
public static final ColumnExp<String> ACCOUNT_NAME = colexp(_R, "accountName");
|
||||
public static final ColumnExp<Integer> ACTION = colexp(_R, "action");
|
||||
public static final ColumnExp<String> DATA = colexp(_R, "data");
|
||||
public static final ColumnExp<Timestamp> ENTERED = colexp(_R, "entered");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The action id. */
|
||||
@Id @Column(name="ACTION_ID") @GeneratedValue(strategy=GenerationType.AUTO)
|
||||
public int actionId;
|
||||
|
||||
/** The account name. */
|
||||
@Column(name="ACCOUNT_NAME")
|
||||
public String accountName;
|
||||
|
||||
/** The action that took place. */
|
||||
@Column(name="ACTION")
|
||||
public int action;
|
||||
|
||||
/** Data that is interpreted depending on the action type. */
|
||||
@Column(name="DATA", length=65536, nullable=true)
|
||||
public String data;
|
||||
|
||||
/** When the action took place. */
|
||||
@Column(name="ENTERED")
|
||||
public Timestamp entered;
|
||||
|
||||
/**
|
||||
* Creates an AccountActionRecord from a AccountAction.
|
||||
*/
|
||||
public static AccountActionRecord fromAccountAction (AccountAction aa)
|
||||
{
|
||||
AccountActionRecord record = new AccountActionRecord();
|
||||
record.actionId = aa.actionId;
|
||||
record.accountName = aa.accountName;
|
||||
record.action = aa.action;
|
||||
record.data = aa.data;
|
||||
record.entered = aa.entered;
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an AccountAction version of this record.
|
||||
*/
|
||||
public AccountAction toAccountAction ()
|
||||
{
|
||||
AccountAction aa = new AccountAction();
|
||||
aa.actionId = actionId;
|
||||
aa.accountName = accountName;
|
||||
aa.action = action;
|
||||
aa.data = data;
|
||||
aa.entered = entered;
|
||||
return aa;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link AccountActionRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<AccountActionRecord> getKey (int actionId)
|
||||
{
|
||||
return newKey(_R, actionId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(ACTION_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.samskivert.depot.DepotRepository;
|
||||
import com.samskivert.depot.Ops;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.clause.Where;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import com.threerings.user.AccountAction;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Provides access to the account actions records.
|
||||
*/
|
||||
@Singleton
|
||||
public class AccountActionRepository extends DepotRepository
|
||||
{
|
||||
/**
|
||||
* The database identifier used when establishing a database connection.
|
||||
* This value being <code>actiondb</code>.
|
||||
*/
|
||||
public static final String ACTION_DB_IDENT = "actiondb";
|
||||
|
||||
@Inject public AccountActionRepository (PersistenceContext ctx)
|
||||
{
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
public AccountActionRepository (ConnectionProvider conprov)
|
||||
{
|
||||
super(new PersistenceContext(ACTION_DB_IDENT, conprov, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of actions that have not yet been processed by the specified server.
|
||||
* Returns all actions if no server name is specified.
|
||||
*
|
||||
* <em>Note:</em> this method has two side effects: the first time it is called, it will
|
||||
* register this server as a action participant (assuming server is non-null) and
|
||||
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
|
||||
* hour.
|
||||
*/
|
||||
public List<AccountAction> getActions (String server)
|
||||
{
|
||||
return getActions(server, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of actions that have not yet been processed by the specified server.
|
||||
* Returns all actions if no server name is specified.
|
||||
*
|
||||
* @param maxActions the maximum number of actions to return.
|
||||
*
|
||||
* <em>Note:</em> this method has two side effects: the first time it is called, it will
|
||||
* register this server as a action participant (assuming server is non-null) and
|
||||
* subsequently, it will call {@link #pruneActions} if it has not done so within the last
|
||||
* hour.
|
||||
*/
|
||||
public List<AccountAction> getActions (String server, int maxActions)
|
||||
{
|
||||
// TODO: Figure out how to do this in one query
|
||||
List<Integer> processedIds = from(ProcessedActionRecord.class)
|
||||
.where(ProcessedActionRecord.SERVER.eq(server))
|
||||
.select(ProcessedActionRecord.ACTION_ID);
|
||||
List<AccountActionRecord> actions = findAll(AccountActionRecord.class,
|
||||
new Where(Ops.not(AccountActionRecord.ACTION_ID.in(processedIds))));
|
||||
|
||||
List<AccountAction> list = Lists.newArrayListWithCapacity(actions.size());
|
||||
for (AccountActionRecord record : actions) {
|
||||
list.add(record.toAccountAction());
|
||||
}
|
||||
|
||||
if (StringUtil.isBlank(server)) {
|
||||
return list;
|
||||
}
|
||||
|
||||
// if this is the first time this method is called, register ourselves
|
||||
// with the action system
|
||||
long now = System.currentTimeMillis();
|
||||
if (_lastActionPrune == 0L) {
|
||||
_lastActionPrune = now;
|
||||
registerActionServer(server);
|
||||
|
||||
} else if (now - _lastActionPrune > ACTION_PRUNE_INTERVAL) {
|
||||
_lastActionPrune = now;
|
||||
pruneActions();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository.
|
||||
*/
|
||||
public void addAction (String accountName, int action)
|
||||
{
|
||||
addAction(accountName, null, action, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository.
|
||||
*/
|
||||
public void addAction (String accountName, String data, int action)
|
||||
{
|
||||
addAction(accountName, data, action, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository.
|
||||
*/
|
||||
public void addAction (String accountName, int action, String server)
|
||||
{
|
||||
addAction(accountName, null, action, server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new action to the repository, with the specified server being marked as already
|
||||
* having processed the action.
|
||||
*/
|
||||
public void addAction (String accountName, String data, int action, final String server)
|
||||
{
|
||||
AccountActionRecord record = new AccountActionRecord();
|
||||
record.accountName = accountName;
|
||||
record.data = data;
|
||||
record.action = action;
|
||||
record.entered = new Timestamp(System.currentTimeMillis());
|
||||
insert(record);
|
||||
|
||||
// note that it was processed by this server if appropriate
|
||||
if (!StringUtil.isBlank(server)) {
|
||||
noteProcessed(record.actionId, server);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the single specified action to reflect that the specified server has processed it.
|
||||
*/
|
||||
public void updateAction (AccountAction action, String server)
|
||||
{
|
||||
updateActions(Collections.singletonList(action), server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update a list of actions.
|
||||
*/
|
||||
public void updateActions (List<AccountAction> actions, String server)
|
||||
{
|
||||
for (int ii = 0, ll = actions.size(); ii < ll; ii++) {
|
||||
noteProcessed(actions.get(ii).actionId, server);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an action from the repository.
|
||||
*/
|
||||
public void deleteAction (AccountAction action)
|
||||
{
|
||||
delete(AccountActionRecord.fromAccountAction(action));
|
||||
deleteAll(ProcessedActionRecord.class,
|
||||
new Where(ProcessedActionRecord.ACTION_ID.eq(action.actionId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes that the specified server has processed the specified action.
|
||||
*/
|
||||
public void noteProcessed (int actionId, String server)
|
||||
{
|
||||
ProcessedActionRecord record = new ProcessedActionRecord();
|
||||
record.actionId = actionId;
|
||||
record.server = server;
|
||||
insert(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes actions that have been processed by all registered servers to keep the actions table
|
||||
* small. This method need not be called by hand as it is called automatically by
|
||||
* {@link #getActions(String,int)}, but no more frequently than once an hour.
|
||||
*/
|
||||
public void pruneActions ()
|
||||
{
|
||||
Set<String> servers = loadActionServers();
|
||||
Map<Integer, Set<String>> procmap = Maps.newHashMap();
|
||||
Set<Integer> actids = Sets.newHashSet();
|
||||
|
||||
// determine which servers have processed which actions
|
||||
for (ProcessedActionRecord record : findAll(ProcessedActionRecord.class)) {
|
||||
Set<String> procset = procmap.get(record.actionId);
|
||||
if (procset == null) {
|
||||
procmap.put(record.actionId, procset = Sets.newHashSet());
|
||||
}
|
||||
procset.add(record.server);
|
||||
}
|
||||
|
||||
// determine which of those have been fully processed
|
||||
for (Map.Entry<Integer, Set<String>> entry : procmap.entrySet()) {
|
||||
if (entry.getValue().containsAll(servers)) {
|
||||
actids.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// now wipe out the actions (and processed entries for all actions that have been fully
|
||||
// processed
|
||||
if (actids.size() > 0) {
|
||||
deleteAll(AccountActionRecord.class,
|
||||
new Where(AccountActionRecord.ACTION_ID.in(actids)));
|
||||
deleteAll(ProcessedActionRecord.class,
|
||||
new Where(ProcessedActionRecord.ACTION_ID.in(actids)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a server with the action system. This method is called automatically the first
|
||||
* time {@link #getActions(String)} is called.
|
||||
*/
|
||||
protected void registerActionServer (String server)
|
||||
{
|
||||
// see if we're already registered
|
||||
Set<String> set = loadActionServers();
|
||||
if (set.contains(server)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ActionServerRecord record = new ActionServerRecord();
|
||||
record.server = server;
|
||||
insert(record);
|
||||
|
||||
log.info("Registered action server", "server", server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up the set of action servers.
|
||||
*/
|
||||
protected Set<String> loadActionServers ()
|
||||
{
|
||||
Set<String> set = Sets.newHashSet();
|
||||
for (ActionServerRecord record : findAll(ActionServerRecord.class)) {
|
||||
set.add(record.server);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
|
||||
{
|
||||
classes.add(AccountActionRecord.class);
|
||||
classes.add(ActionServerRecord.class);
|
||||
classes.add(ProcessedActionRecord.class);
|
||||
}
|
||||
|
||||
/** Used by {@link #getActions(String)}. */
|
||||
protected long _lastActionPrune;
|
||||
|
||||
/** We automatically prune the actions table once an hour. */
|
||||
protected static final long ACTION_PRUNE_INTERVAL = 60 * 60 * 1000L;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* A server that processes actions.
|
||||
*/
|
||||
@Entity(name="ACTION_SERVERS")
|
||||
public class ActionServerRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<ActionServerRecord> _R = ActionServerRecord.class;
|
||||
public static final ColumnExp<String> SERVER = colexp(_R, "server");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The server name. */
|
||||
@Id @Column(name="SERVER")
|
||||
public String server;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link ActionServerRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<ActionServerRecord> getKey (String server)
|
||||
{
|
||||
return newKey(_R, server);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(SERVER); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Computed;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* The number of users referred by an affiliate created on a particular day.
|
||||
*/
|
||||
@Computed(shadowOf=HistoricalUserRecord.class)
|
||||
public class AffiliateCountRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<AffiliateCountRecord> _R = AffiliateCountRecord.class;
|
||||
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
|
||||
public static final ColumnExp<Integer> COUNT = colexp(_R, "count");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** The day that these users were created */
|
||||
public Date created;
|
||||
|
||||
/** The number of users. */
|
||||
@Computed(fieldDefinition="count(*)")
|
||||
public int count;
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.samskivert.servlet.SiteIdentifier;
|
||||
import com.samskivert.servlet.util.CookieUtil;
|
||||
import com.samskivert.servlet.util.ParameterUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import com.threerings.user.AffiliateUtils;
|
||||
import com.threerings.user.OOOUser;
|
||||
|
||||
public abstract class AffiliateInfo
|
||||
{
|
||||
/** The name of the session attribute used to hold the affiliate suffix */
|
||||
public static final String AFFILIATE_SUFFIX_ATTRIBUTE =
|
||||
AffiliateInfo.class.getName() + ".AffiliateSuffix";
|
||||
|
||||
/**
|
||||
* Class determining the behavior if the affiliate is 'new referral' i.e. the URL
|
||||
* has a 'from' and optionally, a 'tag' parameter.
|
||||
*/
|
||||
protected static class NewReferral extends AffiliateInfo
|
||||
{
|
||||
protected final String fromParameter;
|
||||
|
||||
protected final int siteId;
|
||||
|
||||
protected final int tagId;
|
||||
|
||||
public NewReferral (String fromParameter, int siteId, int tagId)
|
||||
{
|
||||
this.fromParameter = fromParameter;
|
||||
this.siteId = siteId;
|
||||
this.tagId = tagId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
if (isPersonalSuffix(fromParameter)) {
|
||||
setAffiliateSuffix(req, fromParameter, tagId);
|
||||
} else {
|
||||
setAffiliateSuffix(req, "" + siteId, tagId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return fromParameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return !isPersonalSuffix(fromParameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSiteId ()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSiteId ()
|
||||
{
|
||||
return siteId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
if (isPersonalSuffix(fromParameter)) {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, fromParameter);
|
||||
} else {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// TODO probably need nothing
|
||||
giveCookie(req, rsp, SITE_REFER_COOKIE, fromParameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return tagId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class determining the behavior if the wasn't referred to us in this particular
|
||||
* request, but they were referred to us before, and we've recorded that in cookies.
|
||||
*/
|
||||
protected static class CurrentCookies extends AffiliateInfo
|
||||
{
|
||||
protected final String siteIdCookieValue;
|
||||
|
||||
protected final int tagIdCookieValue;
|
||||
|
||||
public CurrentCookies (String siteIdCookieValue, int tagIdCookieValue)
|
||||
{
|
||||
this.siteIdCookieValue = siteIdCookieValue;
|
||||
this.tagIdCookieValue = tagIdCookieValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
setAffiliateSuffix(req, siteIdCookieValue, tagIdCookieValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSiteId ()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSiteId ()
|
||||
{
|
||||
try {
|
||||
return Integer.parseInt(siteIdCookieValue);
|
||||
} catch (NumberFormatException nfe) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "" + tagIdCookieValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteIdCookieValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, SITE_REFER_COOKIE, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return tagIdCookieValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class determining the behavior if the user has an 'old-style' cookie indicating
|
||||
* that they came to us as a referral.
|
||||
*/
|
||||
protected static class LegacyCookies extends AffiliateInfo
|
||||
{
|
||||
protected final String siteRefer;
|
||||
|
||||
protected final int siteId;
|
||||
|
||||
public LegacyCookies (int siteId, String siteReferCookie)
|
||||
{
|
||||
this.siteRefer = siteReferCookie;
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
if (isPersonalSuffix(siteRefer)) {
|
||||
setAffiliateSuffix(req, siteRefer, AffiliateTagRecord.NO_TAG);
|
||||
} else {
|
||||
setAffiliateSuffix(req, "" + siteId, AffiliateTagRecord.NO_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
if (!isPersonalSuffix(siteRefer)) {
|
||||
return siteRefer;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return !isPersonalSuffix(siteRefer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSiteId ()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSiteId ()
|
||||
{
|
||||
return siteId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// TODO: should we actually be removing the cookie here
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
if (isPersonalSuffix(siteRefer)) {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, siteRefer);
|
||||
} else {
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + siteId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// TODO: should be be removing this cookie?
|
||||
giveCookie(req, rsp, SITE_REFER_COOKIE, siteRefer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return AffiliateTagRecord.NO_TAG;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the behavior if we don't have any affiliate information but we want to track the user
|
||||
* in a separate category from the 'default site'.
|
||||
*/
|
||||
protected static class AlternateDefault extends AffiliateInfo
|
||||
{
|
||||
protected final int site;
|
||||
|
||||
protected AlternateDefault (int site)
|
||||
{
|
||||
this.site = site;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDefaultAffiliate ()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
setAffiliateSuffix(req, "" + site, AffiliateTagRecord.NO_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return AffiliateTagRecord.NO_TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSiteId ()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSiteId ()
|
||||
{
|
||||
return site;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// Don't issue a cookie;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_ID_COOKIE, "" + site);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// Don't issue a cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the behavior if a tag cookie exists, but not an affiliate (site id) cookie.
|
||||
* "0" will be supplied as the affiliate to the 'affsuf' context attribute.
|
||||
*/
|
||||
protected static class TagOnly extends AffiliateInfo
|
||||
{
|
||||
protected final int tagId;
|
||||
|
||||
public TagOnly (int tagId)
|
||||
{
|
||||
this.tagId = tagId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToContext (HttpServletRequest req)
|
||||
{
|
||||
setAffiliateSuffix(req, "0", tagId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliateName ()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasName ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSiteId ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSiteId ()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
giveCookie(req, rsp, AffiliateUtils.AFFILIATE_TAG_COOKIE, String.valueOf(tagId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// do not issue a cookie
|
||||
}
|
||||
|
||||
@Override
|
||||
public void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// do not issue a cookie
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTagId ()
|
||||
{
|
||||
return tagId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out whether we know the name of the affiliate.
|
||||
*/
|
||||
public abstract boolean hasName ();
|
||||
|
||||
/**
|
||||
* Get the name of the affiliate.
|
||||
*
|
||||
* @return - The name as a string.
|
||||
*/
|
||||
public abstract String getAffiliateName ();
|
||||
|
||||
/**
|
||||
* Find out whether we know the site id of the affiliate
|
||||
*/
|
||||
public abstract boolean hasSiteId ();
|
||||
|
||||
/**
|
||||
* Get the site id of the affiliate.
|
||||
*
|
||||
* @return - the site id of the affiliate
|
||||
*/
|
||||
public abstract int getSiteId ();
|
||||
|
||||
/**
|
||||
* Get the id for the affiliate tag.
|
||||
*/
|
||||
public abstract int getTagId ();
|
||||
|
||||
/**
|
||||
* Compute the appropriate affiliate 'affsuf' and add it to the session along with the
|
||||
* 'affid' used by some of the web templates. Now with less Velocity.
|
||||
*/
|
||||
public abstract void addToContext (HttpServletRequest req);
|
||||
|
||||
/**
|
||||
* Issue the site_id cookie to the browser (non-velocity).
|
||||
*/
|
||||
public abstract void issueSiteIdCookie (HttpServletRequest req, HttpServletResponse rsp);
|
||||
|
||||
/**
|
||||
* Issue the site_refer cookie to the browser.
|
||||
*/
|
||||
public abstract void issueSiteReferCookie (HttpServletRequest req, HttpServletResponse rsp);
|
||||
|
||||
/**
|
||||
* Issue the affiliate_tag cookie to the browser (non-velocity).
|
||||
*/
|
||||
public abstract void issueAffiliateTagCookie (HttpServletRequest req, HttpServletResponse rsp);
|
||||
|
||||
/**
|
||||
* Normally returns false, but can be overridden to indicate that we've been given
|
||||
* a cookie with a default affiliate passed in to us. "Default" isn't quite the right word
|
||||
* since this is really used for letting pages hardwire their default affiliate to be something
|
||||
* other than the default, but everything else uses this word, and I don't have a better idea
|
||||
* offhand.
|
||||
*/
|
||||
public boolean isDefaultAffiliate ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an affiliate info object from the cookies and parameters in the request.
|
||||
*/
|
||||
public static AffiliateInfo getAffiliateInfo (
|
||||
DepotUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
|
||||
HttpServletRequest req, boolean parseFromParameter)
|
||||
{
|
||||
int siteId = identifier.identifySite(req);
|
||||
return getAffiliateInfo(usermgr, repository, identifier, req, siteId, parseFromParameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an affiliate info object from the cookies and parameters in the request.
|
||||
*
|
||||
* @param alternateDefault - override default affiliate (hard-coded into affiliate pages)
|
||||
* @param parseFromParameter - Should we look at the request parameter "from"?
|
||||
* @return - The affiliate info or null if there wasn't enough information in the request to
|
||||
* construct it.
|
||||
*/
|
||||
public static AffiliateInfo getAffiliateInfo (
|
||||
DepotUserManager usermgr, ReferralRepository repository, SiteIdentifier identifier,
|
||||
HttpServletRequest req, int alternateDefault, boolean parseFromParameter)
|
||||
{
|
||||
// if they're already a user with us, bail
|
||||
if (null != usermgr.loadUser(req)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String fromParameter = parseFromParameter ? parseFromParameter(req) : "";
|
||||
|
||||
// is this a new referral - i.e. this request was referred from a referral link?
|
||||
if (!StringUtil.isBlank(fromParameter)) {
|
||||
String tagParameter = readAffiliateTag(req);
|
||||
|
||||
int siteId = parseReferrer(repository, identifier, fromParameter);
|
||||
if (siteId == 0) {
|
||||
log.warning("User referred by bogus referrer [referrer=" + fromParameter + "].");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!StringUtil.isBlank(tagParameter) && !isPersonalSuffix(fromParameter)) {
|
||||
int tagId = usermgr.getAffiliateTagId(tagParameter);
|
||||
|
||||
return new NewReferral(fromParameter, siteId, tagId);
|
||||
} else {
|
||||
return new NewReferral(fromParameter, siteId, AffiliateTagRecord.NO_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
// do we have modern cookies for this referral?
|
||||
String siteIdCookieValue = readSiteIdCookie(req);
|
||||
if (!StringUtil.isBlank(siteIdCookieValue)) {
|
||||
int tagIdCookieValue = readTagId(req);
|
||||
|
||||
return new CurrentCookies(siteIdCookieValue, tagIdCookieValue);
|
||||
}
|
||||
|
||||
// do we have old fashioned cookies for this referral?
|
||||
String siteReferCookie = CookieUtil.getCookieValue(req, SITE_REFER_COOKIE);
|
||||
if (!StringUtil.isBlank(siteReferCookie)) {
|
||||
int siteId = parseReferrer(repository, identifier, siteReferCookie);
|
||||
return new LegacyCookies(siteId, siteReferCookie);
|
||||
}
|
||||
|
||||
// we have no useful affiliate information. If we have been provided with an alternative
|
||||
// default site, then use that as the site-id.
|
||||
if (alternateDefault != identifier.identifySite(req)) {
|
||||
return new AlternateDefault(alternateDefault);
|
||||
}
|
||||
|
||||
// we have a tag cookie but no affiliate cookie
|
||||
int tagIdCookieValue = readTagId(req);
|
||||
if (tagIdCookieValue != AffiliateTagRecord.NO_TAG) {
|
||||
return new TagOnly(tagIdCookieValue);
|
||||
}
|
||||
|
||||
// we couldn't figure out anything useful to do with whatever affiliate related info
|
||||
// we had, so we return null and the caller can do nothing.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param repository For verifying referrer ids against the database
|
||||
* @param identifier Velocity site repository
|
||||
* @param referrer Referral request string, may be a personal referrer of
|
||||
* the format r[0-9]+ or user-[0-9]+, or a site referrer of format [0-9]+
|
||||
* @return the referrer siteId, which may be a positive or negative number,
|
||||
* or 0 if no match.
|
||||
*/
|
||||
public static int parseReferrer (ReferralRepository repository, SiteIdentifier identifier,
|
||||
String referrer)
|
||||
{
|
||||
// blank referrer string
|
||||
if (StringUtil.isBlank(referrer)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// handle new-style personal referrals
|
||||
if (referrer.matches("r[0-9]+")) {
|
||||
int refId = 0;
|
||||
try {
|
||||
refId = Integer.parseInt(referrer.substring(1));
|
||||
} catch (NumberFormatException nfe) {
|
||||
log.warning("Bogus user referral: " + referrer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// look up the record in the referrer repository
|
||||
ReferralRecord rec = repository.lookupReferral(refId);
|
||||
return (rec == null) ? OOOUser.DEFAULT_SITE_ID : (-1 * rec.referrerId);
|
||||
}
|
||||
|
||||
// handle old-style personal referrals
|
||||
if (referrer.startsWith("user-")) {
|
||||
try {
|
||||
return -1 * Integer.parseInt(referrer.substring(5));
|
||||
} catch (NumberFormatException nfe) {
|
||||
log.warning("Bogus user referral: " + referrer);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// check referrer string against site repository
|
||||
int site = identifier.getSiteId(referrer);
|
||||
if (site != -1) {
|
||||
return site;
|
||||
}
|
||||
log.warning("Bogus site specified in referrer " + "[value=" + referrer + "].");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the contents of the 'from' parameter.
|
||||
*/
|
||||
public static String parseFromParameter (HttpServletRequest req)
|
||||
{
|
||||
String value = ParameterUtil.getParameter(req, "from", true);
|
||||
if (value != null) {
|
||||
// strip the crap some affiliates append to their ID
|
||||
Matcher am = _affregex.matcher(value);
|
||||
if (am.find()) {
|
||||
value = am.group();
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out whether the affiliate name is actually a personal referral as
|
||||
* opposed to a site referral.
|
||||
*
|
||||
* @return - True if the affiliate name is a personal referral. False if
|
||||
* it's a site or unknown.
|
||||
*/
|
||||
protected static boolean isPersonalSuffix (String name)
|
||||
{
|
||||
return name != null && name.matches("r[0-9]+");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value stored in the site id cookie.
|
||||
*/
|
||||
protected static String readSiteIdCookie (HttpServletRequest req)
|
||||
{
|
||||
return CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_ID_COOKIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and add the affiliate id and affiliate suffix to the session. No longer
|
||||
* added to the Velocity context; this is velocity-independant.
|
||||
* @param req
|
||||
* @param referrer
|
||||
* @param tagId
|
||||
*/
|
||||
protected void setAffiliateSuffix (HttpServletRequest req, String referrer, int tagId)
|
||||
{
|
||||
if (tagId != AffiliateTagRecord.NO_TAG) {
|
||||
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer + "-" + tagId);
|
||||
} else {
|
||||
req.getSession().setAttribute(AFFILIATE_SUFFIX_ATTRIBUTE, "-" + referrer);
|
||||
}
|
||||
|
||||
req.getSession().setAttribute(AffiliateUtils.AFFILIATE_ID_ATTRIBUTE, referrer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a cookie to the response (velocity independant).
|
||||
* @param req servlet request object
|
||||
* @param rsp sevlet response object
|
||||
* @param name cookie identifier
|
||||
* @param value value for the cookie
|
||||
*/
|
||||
protected static void giveCookie (HttpServletRequest req, HttpServletResponse rsp,
|
||||
String name, String value)
|
||||
{
|
||||
Cookie cookie = new Cookie(name, value);
|
||||
// cookie expires in one month
|
||||
cookie.setMaxAge(30 * 24 * 60 * 60);
|
||||
// set the path and widened domain (eg www.domain.com -> .domain.com) of the cookie
|
||||
cookie.setPath("/");
|
||||
CookieUtil.widenDomain(req, cookie);
|
||||
rsp.addCookie(cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the affiliate tag from the tag parameter in the request.
|
||||
*
|
||||
* @param req - The request.
|
||||
* @return - The tag string.
|
||||
*/
|
||||
protected static String readAffiliateTag (HttpServletRequest req)
|
||||
{
|
||||
return req.getParameter("tag");
|
||||
}
|
||||
|
||||
protected static int readTagId (HttpServletRequest req)
|
||||
{
|
||||
// if they have an affiliate tag cookie, get it.
|
||||
String cookie = CookieUtil.getCookieValue(req, AffiliateUtils.AFFILIATE_TAG_COOKIE);
|
||||
if (cookie != null) {
|
||||
try {
|
||||
return Integer.parseInt(cookie);
|
||||
} catch (NumberFormatException e) {
|
||||
return AffiliateTagRecord.NO_TAG;
|
||||
}
|
||||
} else {
|
||||
return AffiliateTagRecord.NO_TAG;
|
||||
}
|
||||
}
|
||||
|
||||
/** A regexp that matches just the proper parts of an affiliate id. */
|
||||
protected static Pattern _affregex = Pattern.compile(OOOUser.SITE_STRING_REGEX);
|
||||
|
||||
/**
|
||||
* The name of the legacy cookie used to store the affiliate name passed by
|
||||
* them to us with their original request.
|
||||
*/
|
||||
protected static final String SITE_REFER_COOKIE = "site_refer";
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.GenerationType;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Maintains a mapping from an arbitrary text tag to an integer identifier.
|
||||
*/
|
||||
@Entity(name="afftags")
|
||||
public class AffiliateTagRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<AffiliateTagRecord> _R = AffiliateTagRecord.class;
|
||||
public static final ColumnExp<Integer> TAG_ID = colexp(_R, "tagId");
|
||||
public static final ColumnExp<String> TAG = colexp(_R, "tag");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** Value indicating that the affiliate did not provide a tag **/
|
||||
public static final int NO_TAG = 0;
|
||||
|
||||
/** The automatically generated tag id. */
|
||||
@Id @GeneratedValue(strategy=GenerationType.AUTO)
|
||||
public int tagId;
|
||||
|
||||
/** The arbitrary text tag. */
|
||||
@Column(unique=true)
|
||||
public String tag;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link AffiliateTagRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<AffiliateTagRecord> getKey (int tagId)
|
||||
{
|
||||
return newKey(_R, tagId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(TAG_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Computed;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
@Computed(shadowOf=HistoricalUserRecord.class)
|
||||
public class AffiliateUserCountRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<AffiliateUserCountRecord> _R = AffiliateUserCountRecord.class;
|
||||
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
|
||||
public static final ColumnExp<Integer> COUNT = colexp(_R, "count");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** The affiliate site ID. */
|
||||
public int siteId;
|
||||
|
||||
/** The number of users. */
|
||||
@Computed(fieldDefinition="count(*)")
|
||||
public int count;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Represents a row in the BANNED_IDENTS table, Depot version.
|
||||
*/
|
||||
@Entity(name="BANNED_IDENTS")
|
||||
public class BannedIdentRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<BannedIdentRecord> _R = BannedIdentRecord.class;
|
||||
public static final ColumnExp<String> MACH_IDENT = colexp(_R, "machIdent");
|
||||
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** a 'unique' id for the specific machine we have seen. */
|
||||
@Id @Column(name="MACH_IDENT")
|
||||
public String machIdent;
|
||||
|
||||
/** The site if which this machine is banned from. */
|
||||
@Id @Column(name="SITE_ID")
|
||||
public int siteId;
|
||||
|
||||
/** Blank constructor for the unserialization business. */
|
||||
public BannedIdentRecord ()
|
||||
{
|
||||
}
|
||||
|
||||
/** A constructor that populates this record. */
|
||||
public BannedIdentRecord (String machIdent, int siteId)
|
||||
{
|
||||
this.machIdent = machIdent;
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link BannedIdentRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<BannedIdentRecord> getKey (String machIdent, int siteId)
|
||||
{
|
||||
return newKey(_R, machIdent, siteId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(MACH_IDENT, SITE_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import com.threerings.user.OOOUser;
|
||||
|
||||
/**
|
||||
* Emulates {@link ConversionRecord} for the Depot.
|
||||
*/
|
||||
@Entity(name="CONVERSION")
|
||||
public class ConversionRecord extends PersistentRecord
|
||||
{
|
||||
/** Indicates that an account was created. */
|
||||
public static final int CREATED = 1;
|
||||
|
||||
/** Indicates that an account that was not currently paying us started
|
||||
* to. This won't be true before 2005/03/01 where we tried (more or
|
||||
* less) to save everytime someone gave us money. */
|
||||
public static final int SUBSCRIPTION_START = 2;
|
||||
|
||||
/** Indicates that a subscription was canceled or lapsed from use. */
|
||||
public static final int SUBSCRIPTION_ENDED = 3;
|
||||
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<ConversionRecord> _R = ConversionRecord.class;
|
||||
public static final ColumnExp<Timestamp> RECORDED = colexp(_R, "recorded");
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
|
||||
public static final ColumnExp<Short> ACTION = colexp(_R, "action");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The timestamp on which the action was taken. */
|
||||
@Id @Column(name="RECORDED")
|
||||
public Timestamp recorded;
|
||||
|
||||
/** The unique identifier of the user that took an action. */
|
||||
@Column(name="USER_ID")
|
||||
public int userId;
|
||||
|
||||
/**
|
||||
* The partner with whom the user is associated. You should always
|
||||
* use the accessor method to access this value in order to correctly
|
||||
* deal with personal referrals.
|
||||
*/
|
||||
@Column(name="SITE_ID")
|
||||
public int siteId;
|
||||
|
||||
/** The identifier indicating the action taken. */
|
||||
@Column(name="ACTION")
|
||||
public short action;
|
||||
|
||||
/**
|
||||
* Returns the site id associated with this record, properly mapping
|
||||
* negative ids (personal referrals) into a single category.
|
||||
*/
|
||||
public int getSiteId ()
|
||||
{
|
||||
return (siteId <= 0) ? OOOUser.REFERRAL_SITE_ID : siteId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
|
||||
public boolean equals (ConversionRecord cr)
|
||||
{
|
||||
return (cr.userId == userId && cr.siteId == siteId &&
|
||||
cr.action == action && cr.recorded.equals(recorded));
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link ConversionRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<ConversionRecord> getKey (Timestamp recorded)
|
||||
{
|
||||
return newKey(_R, recorded);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(RECORDED); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.samskivert.depot.DepotRepository;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.clause.OrderBy;
|
||||
import com.samskivert.depot.clause.Where;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.util.IntIntMap;
|
||||
import com.samskivert.util.Calendars;
|
||||
|
||||
/**
|
||||
* Provides an interface to the conversion repository. A service that
|
||||
* makes use of the OOO user management and billing system can make use of
|
||||
* this conversion service to track conversion related information for
|
||||
* their users.
|
||||
*/
|
||||
@Singleton
|
||||
public class ConversionRepository extends DepotRepository
|
||||
{
|
||||
/**
|
||||
* The database identifier used when establishing a database
|
||||
* connection. This value being <code>conversiondb</code>.
|
||||
*/
|
||||
public static final String CONVERSION_DB_IDENT = "conversiondb";
|
||||
|
||||
@Inject public ConversionRepository (PersistenceContext ctx)
|
||||
{
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the repository and prepares it for operation.
|
||||
*
|
||||
* @param conprov the database connection provider.
|
||||
*/
|
||||
public ConversionRepository (ConnectionProvider conprov)
|
||||
{
|
||||
super(new PersistenceContext(CONVERSION_DB_IDENT, conprov, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an action in the conversion table.
|
||||
*/
|
||||
public void recordAction (int userId, int siteId, int action)
|
||||
{
|
||||
recordAction(userId, siteId, action, new Timestamp(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an action in the conversion table.
|
||||
*/
|
||||
public void recordAction (int userId, int siteId, int action, Timestamp recorded)
|
||||
{
|
||||
ConversionRecord record = new ConversionRecord();
|
||||
record.userId = userId;
|
||||
record.siteId = siteId;
|
||||
record.action = (short)action;
|
||||
record.recorded = recorded;
|
||||
insert(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscriber info for a given date broken up by site. Key 0
|
||||
* references a total for all sites.
|
||||
*/
|
||||
public IntIntMap getSiteSubscribers (java.util.Date date)
|
||||
{
|
||||
checkRecomputeSubInfo();
|
||||
return _subscribers.get(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total number of subscribers on then given date.
|
||||
*/
|
||||
public int getTotalSubscribers (java.util.Date date)
|
||||
{
|
||||
checkRecomputeSubInfo();
|
||||
return _subscribers.get(date).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of our subscription info. Date -> IntIntMap,
|
||||
* IntIntMap: SiteId -> Subscribers. SiteId 0 is a total of all sites.
|
||||
*/
|
||||
public Map<Date, IntIntMap> getSubscriptionInfo ()
|
||||
{
|
||||
checkRecomputeSubInfo();
|
||||
return _subscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute our subscription info if it is older than the cache time.
|
||||
*/
|
||||
protected void checkRecomputeSubInfo ()
|
||||
{
|
||||
if (_subTime + SUB_CACHE_TIME < System.currentTimeMillis()) {
|
||||
computeSubscriptionInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up all the subscriber data over time and affiliate.
|
||||
*/
|
||||
protected void computeSubscriptionInfo ()
|
||||
{
|
||||
// get all the raw data from the db, ordered by date
|
||||
List<ConversionRecord> data = findAll(ConversionRecord.class,
|
||||
new Where(ConversionRecord.ACTION.in(ConversionRecord.SUBSCRIPTION_START,
|
||||
ConversionRecord.SUBSCRIPTION_ENDED)),
|
||||
OrderBy.ascending(ConversionRecord.RECORDED));
|
||||
|
||||
java.util.Date recent = null;
|
||||
|
||||
Set<Integer> subs = Sets.newHashSet();
|
||||
Map<Integer, Set<Integer>> siteSubs = Maps.newHashMap();
|
||||
|
||||
// these are used to do the right thing if we get both a subscribe
|
||||
// and unsubscribe action in the same day, but in the wrong order
|
||||
Set<Integer> subexcept = Sets.newHashSet();
|
||||
Set<Integer> unsubexcept = Sets.newHashSet();
|
||||
|
||||
for (ConversionRecord cr : data) {
|
||||
// we only care about the date, not the time, at which the
|
||||
// entry was recorded since we are hashing by day
|
||||
java.util.Date day = Calendars.at(cr.recorded).zeroTime().toDate();
|
||||
|
||||
// if the date changes, store the data for the most recent day
|
||||
if (recent == null) {
|
||||
recent = day;
|
||||
} else if (!recent.equals(day)) {
|
||||
subexcept.clear();
|
||||
unsubexcept.clear();
|
||||
addSubscriptionEntry(recent, subs, siteSubs);
|
||||
recent = day;
|
||||
}
|
||||
|
||||
// make sure we have a sitewise set of subscribers
|
||||
if (!siteSubs.containsKey(cr.getSiteId())) {
|
||||
siteSubs.put(cr.getSiteId(), Sets.<Integer>newHashSet());
|
||||
}
|
||||
Set<Integer> siteMap = siteSubs.get(cr.getSiteId());
|
||||
|
||||
// update our data
|
||||
if (cr.action == ConversionRecord.SUBSCRIPTION_START) {
|
||||
// handle not-subscribed -> unsubscribe -> subscribe
|
||||
if (!checkExcept(unsubexcept, subexcept,
|
||||
subs.contains(cr.userId), cr)) {
|
||||
subs.add(cr.userId);
|
||||
siteMap.add(cr.userId);
|
||||
}
|
||||
|
||||
} else if (cr.action == ConversionRecord.SUBSCRIPTION_ENDED) {
|
||||
// handle is-subscribed -> subscribe -> unsubscribe
|
||||
if (!checkExcept(subexcept, unsubexcept,
|
||||
!subs.contains(cr.userId), cr)) {
|
||||
subs.remove(cr.userId);
|
||||
siteMap.remove(cr.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add in the incomplete current day info
|
||||
addSubscriptionEntry(Calendars.now().zeroTime().toDate(), subs, siteSubs);
|
||||
|
||||
// and mark when we finished
|
||||
_subTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
protected boolean checkExcept (
|
||||
Set<Integer> expect, Set<Integer> surprise, boolean currentStatus, ConversionRecord cr)
|
||||
{
|
||||
// first check to see if he's in the opposite exception set; in
|
||||
// which case we clear him out and ignore this action
|
||||
if (expect.remove(cr.userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// next, check to see if he's already in the expected state, in which
|
||||
// case we put him into the exception set
|
||||
return currentStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the _subscriptions data for the given day.
|
||||
*/
|
||||
protected void addSubscriptionEntry (
|
||||
java.util.Date day, Set<Integer> subs, Map<Integer, Set<Integer>> siteSubs)
|
||||
{
|
||||
IntIntMap daysubs = new IntIntMap();
|
||||
for (Map.Entry<Integer, Set<Integer>> entry : siteSubs.entrySet()) {
|
||||
Set<Integer> set = entry.getValue();
|
||||
int key = entry.getKey().intValue();
|
||||
daysubs.put(key, set.size());
|
||||
}
|
||||
daysubs.put(0, subs.size());
|
||||
_subscribers.put(day, daysubs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
|
||||
{
|
||||
classes.add(ConversionRecord.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains subscription info about our users over time. Keys are
|
||||
* dates and the values are IntIntMaps mapping siteId -> subsrivers
|
||||
* with 0 holding the total for all sites.
|
||||
*/
|
||||
protected Map<Date, IntIntMap> _subscribers = Maps.newHashMap();
|
||||
|
||||
/** The time at which we last computed our subscriber info. */
|
||||
protected long _subTime;
|
||||
|
||||
/** The time in millis to cache our subscriber info before recomputing. */
|
||||
protected long SUB_CACHE_TIME = 1l * 60l * 60l * 1000l;
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import com.samskivert.depot.DatabaseException;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
import com.samskivert.servlet.RedirectException;
|
||||
import com.samskivert.servlet.user.AuthenticationFailedException;
|
||||
import com.samskivert.servlet.user.Authenticator;
|
||||
import com.samskivert.servlet.user.InvalidPasswordException;
|
||||
import com.samskivert.servlet.user.NoSuchUserException;
|
||||
import com.samskivert.servlet.user.Password;
|
||||
import com.samskivert.servlet.user.User;
|
||||
import com.samskivert.servlet.util.CookieUtil;
|
||||
import com.samskivert.servlet.util.RequestUtils;
|
||||
import com.samskivert.util.Interval;
|
||||
import com.samskivert.util.RunQueue;
|
||||
import com.samskivert.util.StringUtil;
|
||||
import com.samskivert.util.Tuple;
|
||||
|
||||
import com.threerings.user.OOOUser;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* A OOOUserManager using a depot repository.
|
||||
*/
|
||||
public class DepotUserManager
|
||||
{
|
||||
/** An instance of the insecure authenticator for general-purpose use. */
|
||||
public static final Authenticator AUTH_INSECURE = new InsecureAuthenticator();
|
||||
|
||||
/** An instance of the password authenticator for general-purpose use. */
|
||||
public static final Authenticator AUTH_PASSWORD = new PasswordAuthenticator();
|
||||
|
||||
/**
|
||||
* A totally insecure authenticator that authenticates any user. <em>Note:</em> Applications
|
||||
* that make use of this authenticator should make sure the user has already been authenticated
|
||||
* through some other means.
|
||||
*/
|
||||
public static class InsecureAuthenticator implements Authenticator
|
||||
{
|
||||
// documentation inherited
|
||||
public void authenticateUser (User user, String username, Password password)
|
||||
throws InvalidPasswordException
|
||||
{
|
||||
// don't care
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An authenticator that requires that the user-supplied password match the actual user
|
||||
* password.
|
||||
*/
|
||||
public static class PasswordAuthenticator implements Authenticator
|
||||
{
|
||||
// documentation inherited
|
||||
public void authenticateUser (User user, String username, Password password)
|
||||
throws AuthenticationFailedException
|
||||
{
|
||||
if (!user.passwordsMatch(password)) {
|
||||
throw new InvalidPasswordException("error.invalid_password");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates our Depot user manager and prepares it for operation.
|
||||
*/
|
||||
public DepotUserManager (Properties config, ConnectionProvider conprov)
|
||||
throws DatabaseException
|
||||
{
|
||||
this(config, new PersistenceContext("userdb", conprov, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates our Depot user manager and prepares it for operation.
|
||||
*/
|
||||
public DepotUserManager (Properties config, PersistenceContext pctx)
|
||||
throws DatabaseException
|
||||
{
|
||||
this(config, pctx, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates our Depot user manager and prepares it for operation.
|
||||
*/
|
||||
public DepotUserManager (Properties config, PersistenceContext pctx, RunQueue pruneQueue)
|
||||
throws DatabaseException
|
||||
{
|
||||
init(config, pctx, pruneQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a depot user manager which must subsequently be initialized with a call to
|
||||
* {@link #init}.
|
||||
*/
|
||||
public DepotUserManager ()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this user manager for operation. Presently the user manager requires the
|
||||
* following configuration information:
|
||||
*
|
||||
* <ul>
|
||||
* <li><code>login_url</code>: Should be set to the URL to which to redirect a requester if
|
||||
* they are required to login before accessing the requested page. For example:
|
||||
*
|
||||
* <pre>
|
||||
* login_url = /usermgmt/login.ajsp?return=%R
|
||||
* </pre>
|
||||
*
|
||||
* The <code>%R</code> will be replaced with the URL encoded URL the user is currently
|
||||
* requesting (complete with query parameters) so that the login code can redirect the user
|
||||
* back to this request once they are authenticated.
|
||||
* </ul>
|
||||
*
|
||||
* @param config the user manager configuration properties.
|
||||
* @param pctx the persistence context
|
||||
*/
|
||||
public void init (Properties config, PersistenceContext pctx)
|
||||
throws DatabaseException
|
||||
{
|
||||
init(config, pctx, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this user manager for operation. See {@link #init(Properties,PersistenceContext)}.
|
||||
*
|
||||
* @param pruneQueue an optional run queue on which to run our periodic session pruning task.
|
||||
*/
|
||||
public void init (Properties config, PersistenceContext pctx, RunQueue pruneQueue)
|
||||
throws DatabaseException
|
||||
{
|
||||
// save this for later
|
||||
_config = config;
|
||||
|
||||
// create the user repository
|
||||
_repository = createRepository(pctx);
|
||||
|
||||
// fetch the login URL from the properties
|
||||
_loginURL = config.getProperty("login_url");
|
||||
if (_loginURL == null) {
|
||||
log.warning("No login_url supplied in user manager config. Authentication won't work.");
|
||||
_loginURL = "/missing_login_url";
|
||||
}
|
||||
|
||||
// look up any override to our user auth cookie
|
||||
String authCook = config.getProperty("auth_cookie.name");
|
||||
if (!StringUtil.isBlank(authCook)) {
|
||||
_userAuthCookie = authCook;
|
||||
}
|
||||
|
||||
if (USERMGR_DEBUG) {
|
||||
log.info("UserManager initialized", "acook", _userAuthCookie, "login", _loginURL);
|
||||
}
|
||||
|
||||
// register a cron job to prune the session table every hour
|
||||
_pruner = new Interval() {
|
||||
@Override public void expired () {
|
||||
_repository.pruneSessions();
|
||||
}
|
||||
};
|
||||
if (pruneQueue != null) {
|
||||
_pruner.setRunQueue(pruneQueue);
|
||||
}
|
||||
_pruner.schedule(SESSION_PRUNE_INTERVAL, true);
|
||||
|
||||
// look up the access denied URL
|
||||
_accessDeniedURL = config.getProperty("access_denied_url");
|
||||
if (_accessDeniedURL == null) {
|
||||
log.warning("No 'access_denied_url' supplied in user manager config. " +
|
||||
"Restricted pages will behave strangely.");
|
||||
}
|
||||
|
||||
// load up our affiliate tag mappings
|
||||
for (AffiliateTagRecord sub : getRepository().loadAffiliateTags()) {
|
||||
_tagMap.put(sub.tag, sub.tagId);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown ()
|
||||
{
|
||||
// cancel our session table pruning thread
|
||||
_pruner.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the repository in use by this user manager.
|
||||
*/
|
||||
public DepotUserRepository getRepository ()
|
||||
{
|
||||
return _repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id to which the specified tag has been mapped, assigning a new tag id if
|
||||
* necessary.
|
||||
*
|
||||
* @return -1 if an error occurred assigning a new id.
|
||||
*/
|
||||
public int getAffiliateTagId (String tag)
|
||||
{
|
||||
// if we've already mapped this value, we're good to go
|
||||
Integer tagId = _tagMap.get(tag);
|
||||
if (tagId != null) {
|
||||
return tagId;
|
||||
}
|
||||
|
||||
// register a new affiliate tag and add it to our mappings
|
||||
tagId = getRepository().registerAffiliateTag(tag);
|
||||
_tagMap.put(tag, tagId);
|
||||
return tagId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Both tag strings and ids are unique, so grab the string (key) based on the id (value)
|
||||
* @param tagId ID to search for (eg 45)
|
||||
* @return String value of the tag or tags (eg "nov07GroupA::DF34A9")
|
||||
*/
|
||||
public String getAffiliateTagString (int tagId)
|
||||
{
|
||||
Integer theTagId = Integer.valueOf(tagId);
|
||||
|
||||
for (Map.Entry<String,Integer> entry : _tagMap.entrySet()) {
|
||||
if (theTagId.equals(entry.getValue())) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authentication token for the given request
|
||||
*/
|
||||
public String getAuthToken (HttpServletRequest req)
|
||||
{
|
||||
return CookieUtil.getCookieValue(req, _userAuthCookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the necessary authentication information from the http request and loads the user
|
||||
* identified by that information.
|
||||
*
|
||||
* @return the user associated with the request or null if no user was associated with the
|
||||
* request or if the authentication information is bogus.
|
||||
*/
|
||||
public OOOUser loadUser (HttpServletRequest req)
|
||||
{
|
||||
String authcook = getAuthToken(req);
|
||||
if (USERMGR_DEBUG) {
|
||||
log.info("Loading user by cookie", _userAuthCookie, authcook);
|
||||
}
|
||||
return loadUser(authcook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up a user based on the supplied session authentication token.
|
||||
*/
|
||||
public OOOUser loadUser (String authcode)
|
||||
{
|
||||
OOOUser user = (authcode == null) ? null : _repository.loadUserBySession(authcode, false);
|
||||
if (USERMGR_DEBUG) {
|
||||
log.info("Loaded user by authcode", "code", authcode, "user", user);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the necessary authentication information from the http request and loads the user
|
||||
* identified by that information. If no user could be loaded (because the requester is not
|
||||
* authenticated), a redirect exception will be thrown to redirect the user to the login page
|
||||
* specified in the user manager configuration.
|
||||
*
|
||||
* @return the user associated with the request.
|
||||
*/
|
||||
public OOOUser requireUser (HttpServletRequest req)
|
||||
throws RedirectException
|
||||
{
|
||||
OOOUser user = loadUser(req);
|
||||
// if no user was loaded, we need to redirect these fine people to the login page
|
||||
if (user == null) {
|
||||
String eurl = RequestUtils.getLocationEncoded(req);
|
||||
String target = _loginURL.replace("%R", eurl);
|
||||
if (USERMGR_DEBUG) {
|
||||
log.info("No user found in require, redirecting", "to", target);
|
||||
}
|
||||
throw new RedirectException(target);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the standard {@link #requireUser(HttpServletRequest)} with
|
||||
* the additional requirement that the user hold the specified token.
|
||||
* If they do not, they will be redirected to a page informing them
|
||||
* access is denied.
|
||||
*
|
||||
* @return the user associated with the request.
|
||||
*/
|
||||
public OOOUser requireUser (HttpServletRequest req, byte token)
|
||||
throws RedirectException
|
||||
{
|
||||
OOOUser user = requireUser(req);
|
||||
if (!user.holdsToken(token)) {
|
||||
throw new RedirectException(_accessDeniedURL);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the standard {@link #requireUser(HttpServletRequest)} with
|
||||
* the additional requirement that the user hold one of the specified
|
||||
* tokens. If they do not, they will be redirected to a page informing
|
||||
* them access is denied.
|
||||
*
|
||||
* @return the user associated with the request.
|
||||
*/
|
||||
public OOOUser requireUser (HttpServletRequest req, byte[] tokens)
|
||||
throws RedirectException
|
||||
{
|
||||
OOOUser user = requireUser(req);
|
||||
if (!user.holdsAnyToken(tokens)) {
|
||||
throw new RedirectException(_accessDeniedURL);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authenticate the requester and initiate an authenticated session for them. An
|
||||
* authenticated session involves their receiving a cookie that proves them to be authenticated
|
||||
* and an entry in the session database being created that maps their information to their
|
||||
* userid. If this call completes, the session was established and the proper cookies were set
|
||||
* in the supplied response object. If invalid authentication information is provided or some
|
||||
* other error occurs, an exception will be thrown.
|
||||
*
|
||||
* @param username The username supplied by the user.
|
||||
* @param password The password supplied by the user.
|
||||
* @param persist If true, the cookie will expire in one month, if false, the cookie will
|
||||
* expire at the end of the user's browser session.
|
||||
* @param req The request via which the login page was loaded.
|
||||
* @param rsp The response in which the cookie is to be set.
|
||||
* @param auth The authenticator used to check whether the user should be authenticated.
|
||||
*
|
||||
* @return the user object of the authenticated user.
|
||||
*/
|
||||
public OOOUser login (String username, Password password, boolean persist,
|
||||
HttpServletRequest req, HttpServletResponse rsp, Authenticator auth)
|
||||
throws AuthenticationFailedException
|
||||
{
|
||||
// load up the requested user
|
||||
OOOUser user = _repository.loadUser(username);
|
||||
if (user == null) {
|
||||
throw new NoSuchUserException("error.no_such_user");
|
||||
}
|
||||
|
||||
// run the user through the authentication gamut
|
||||
auth.authenticateUser(user, username, password);
|
||||
|
||||
// give them the necessary cookies and business
|
||||
effectLogin(user, persist, req, rsp);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authenticate the requester and initiate an authenticated session for them. A
|
||||
* session token will be assigned to the user and returned along with the associated {@link
|
||||
* User} record. It is assumed that the client will maintain the session token via its own
|
||||
* means.
|
||||
*
|
||||
* @param username the username supplied by the user.
|
||||
* @param password the password supplied by the user.
|
||||
* @param expires the number of days in which this session should expire.
|
||||
* @param auth the authenticator used to check whether the user should be authenticated.
|
||||
*
|
||||
* @return the user object of the authenticated user.
|
||||
*/
|
||||
public Tuple<OOOUser,String> login (
|
||||
String username, Password password, int expires, Authenticator auth)
|
||||
throws AuthenticationFailedException
|
||||
{
|
||||
// load up the requested user
|
||||
OOOUser user = _repository.loadUser(username);
|
||||
if (user == null) {
|
||||
throw new NoSuchUserException("error.no_such_user");
|
||||
}
|
||||
|
||||
// run the user through the authentication gamut
|
||||
auth.authenticateUser(user, username, password);
|
||||
|
||||
// register a session for this user
|
||||
String authcode = _repository.registerSession(user, expires);
|
||||
if (USERMGR_DEBUG) {
|
||||
log.info("Session started", "user", username, "code", authcode);
|
||||
}
|
||||
return new Tuple<OOOUser,String>(user, authcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a user is already known to be authenticated for one reason or other, this method can be
|
||||
* used to give them the appropriate authentication cookies to effect their login.
|
||||
*
|
||||
* @param persist If true, the cookie will expire in one month, if false, the cookie will
|
||||
* expire at the end of the user's browser session.
|
||||
* @return The registered session authcode
|
||||
*/
|
||||
public String effectLogin (
|
||||
OOOUser user, boolean persist, HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
return effectLogin(user, persist ? PERSIST_EXPIRE_DAYS : NON_PERSIST_EXPIRE_DAYS, req, rsp);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a user is already known to be authenticated for one reason or other, this method can be
|
||||
* used to give them the appropriate authentication cookies to effect their login.
|
||||
*
|
||||
* @param expires the number of days in which to expire the session cookie, 0 means expire at
|
||||
* the end of the browser session.
|
||||
* @return The registered session authcode
|
||||
*/
|
||||
public String effectLogin (
|
||||
OOOUser user, int expires, HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
String authcode = _repository.registerSession(user, Math.max(expires, 1));
|
||||
Cookie acookie = new Cookie(_userAuthCookie, authcode);
|
||||
// strip the hostname from the server and use that as the domain unless configured not to
|
||||
if (!"false".equalsIgnoreCase(_config.getProperty("auth_cookie.strip_hostname"))) {
|
||||
CookieUtil.widenDomain(req, acookie);
|
||||
}
|
||||
acookie.setPath("/");
|
||||
acookie.setMaxAge((expires > 0) ? (expires*24*60*60) : -1);
|
||||
if (USERMGR_DEBUG) {
|
||||
log.info("Setting cookie " + acookie + ".");
|
||||
}
|
||||
rsp.addCookie(acookie);
|
||||
return authcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out.
|
||||
*/
|
||||
public void logout (HttpServletRequest req, HttpServletResponse rsp)
|
||||
{
|
||||
// nothing to do if they don't already have an auth cookie
|
||||
String authcode = CookieUtil.getCookieValue(req, _userAuthCookie);
|
||||
if (authcode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set them up the bomb
|
||||
Cookie c = new Cookie(_userAuthCookie, "x");
|
||||
c.setPath("/");
|
||||
c.setMaxAge(0);
|
||||
CookieUtil.widenDomain(req, c);
|
||||
if (USERMGR_DEBUG) {
|
||||
log.info("Clearing cookie " + c + ".");
|
||||
}
|
||||
rsp.addCookie(c);
|
||||
|
||||
// we need an unwidened one to ensure that old-style cookies are wiped as well
|
||||
c = new Cookie(_userAuthCookie, "x");
|
||||
c.setPath("/");
|
||||
c.setMaxAge(0);
|
||||
rsp.addCookie(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the supplied session key is still valid and if so, refreshes it for the
|
||||
* specified number of days.
|
||||
*
|
||||
* @return true if the session was located and refreshed, false otherwise.
|
||||
*/
|
||||
public boolean refreshSession (String sessionKey, int expireDays)
|
||||
{
|
||||
return _repository.refreshSession(sessionKey, expireDays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the user manager to create the user repository. Derived classes can override this
|
||||
* and create a specialized repository if they so desire.
|
||||
*/
|
||||
protected DepotUserRepository createRepository (PersistenceContext pctx)
|
||||
throws DatabaseException
|
||||
{
|
||||
return new DepotUserRepository(pctx);
|
||||
}
|
||||
|
||||
/** Our user manager configuration. */
|
||||
protected Properties _config;
|
||||
|
||||
/** The user repository. */
|
||||
protected DepotUserRepository _repository;
|
||||
|
||||
/** The interval for user session pruning. */
|
||||
protected Interval _pruner;
|
||||
|
||||
/** The URL for the user login page. */
|
||||
protected String _loginURL;
|
||||
|
||||
/** The name of our user authentication cookie. */
|
||||
protected String _userAuthCookie = USERAUTH_COOKIE;
|
||||
|
||||
/** Maintains a mapping of affiliate tags. */
|
||||
protected Map<String,Integer> _tagMap = Maps.newHashMap();
|
||||
|
||||
/** The URL to which we redirect users whose access is denied. */
|
||||
protected String _accessDeniedURL;
|
||||
|
||||
/** The user authentication cookie name. */
|
||||
protected static final String USERAUTH_COOKIE = "id_";
|
||||
|
||||
/** Prune the session table every hour. */
|
||||
protected static final long SESSION_PRUNE_INTERVAL = 60L * 60L * 1000L;
|
||||
|
||||
/** Indicates how long (in days) that a "persisting" session token should last. */
|
||||
protected static final int PERSIST_EXPIRE_DAYS = 30;
|
||||
|
||||
/** Indicates how long (in days) that a "non-persisting" session token should last. */
|
||||
protected static final int NON_PERSIST_EXPIRE_DAYS = 1;
|
||||
|
||||
/** Change this to true and recompile to debug cookie handling. */
|
||||
protected static final boolean USERMGR_DEBUG = false;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Computed;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
import com.threerings.user.DetailedUser;
|
||||
|
||||
/**
|
||||
* A computed persistent entity that loads detailed user information.
|
||||
*/
|
||||
@Computed
|
||||
@Entity
|
||||
public class DetailedUserRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<DetailedUserRecord> _R = DetailedUserRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<String> USERNAME = colexp(_R, "username");
|
||||
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
|
||||
public static final ColumnExp<String> EMAIL = colexp(_R, "email");
|
||||
public static final ColumnExp<Date> BIRTHDAY = colexp(_R, "birthday");
|
||||
public static final ColumnExp<Byte> GENDER = colexp(_R, "gender");
|
||||
public static final ColumnExp<String> MISSIVE = colexp(_R, "missive");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** The user's assigned userid. */
|
||||
@Id @Computed(shadowOf=OOOUserRecord.class)
|
||||
public int userId;
|
||||
|
||||
/** The user's chosen username. */
|
||||
@Computed(shadowOf=OOOUserRecord.class)
|
||||
public String username;
|
||||
|
||||
/** The date this record was created. */
|
||||
@Computed(shadowOf=OOOUserRecord.class)
|
||||
public Date created;
|
||||
|
||||
/** The user's email address. */
|
||||
@Computed(shadowOf=OOOUserRecord.class)
|
||||
public String email;
|
||||
|
||||
/** The user's birthday. */
|
||||
@Column(name="BIRTHDAY") @Computed(shadowOf=OOOAuxDataRecord.class)
|
||||
public Date birthday;
|
||||
|
||||
/** The user's gender. */
|
||||
@Column(name="GENDER") @Computed(shadowOf=OOOAuxDataRecord.class)
|
||||
public byte gender;
|
||||
|
||||
/** The user's personal missive to us. */
|
||||
@Column(name="MISSIVE") @Computed(shadowOf=OOOAuxDataRecord.class)
|
||||
public String missive;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link DetailedUserRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<DetailedUserRecord> getKey (int userId)
|
||||
{
|
||||
return newKey(_R, userId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(USER_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
|
||||
/**
|
||||
* Creates a DetailedUser from the record.
|
||||
*/
|
||||
public DetailedUser toDetailedUser ()
|
||||
{
|
||||
DetailedUser user = new DetailedUser();
|
||||
user.userId = userId;
|
||||
user.username = username;
|
||||
user.created = created;
|
||||
user.email = email;
|
||||
user.birthday = birthday;
|
||||
user.gender = gender;
|
||||
user.missive = missive;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
import com.threerings.user.ExternalAuther;
|
||||
|
||||
/**
|
||||
* Maintains information on a mapping from an external authentication source to a OOOUser.
|
||||
*/
|
||||
public class ExternalAuthRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<ExternalAuthRecord> _R = ExternalAuthRecord.class;
|
||||
public static final ColumnExp<ExternalAuther> AUTHER = colexp(_R, "auther");
|
||||
public static final ColumnExp<String> EXTERNAL_ID = colexp(_R, "externalId");
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<String> SESSION_KEY = colexp(_R, "sessionKey");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** Increment this value if you modify the definition of this persistent object in a way that
|
||||
* will result in a change to its SQL counterpart. */
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The auther that maintains the external user id. */
|
||||
@Id public ExternalAuther auther;
|
||||
|
||||
/** The external user identifier. */
|
||||
@Id public String externalId;
|
||||
|
||||
/** The id of the OOOUser account associated with the specified external account. */
|
||||
@Index(name="ixUserId")
|
||||
public int userId;
|
||||
|
||||
/** The most recent session key provided by the external site, for use in making API requests
|
||||
* to said site based on our most recently active session. */
|
||||
@Column(nullable=true)
|
||||
public String sessionKey;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link ExternalAuthRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<ExternalAuthRecord> getKey (ExternalAuther auther, String externalId)
|
||||
{
|
||||
return newKey(_R, auther, externalId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(AUTHER, EXTERNAL_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.samskivert.depot.DepotRepository;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.expression.FluentExp;
|
||||
import com.samskivert.util.Tuple;
|
||||
|
||||
import com.threerings.user.ExternalAuther;
|
||||
|
||||
/**
|
||||
* Maps authentication information from external sources (like Facebook, OAuth providers, etc.) to
|
||||
* our internal (ooouser) userId number.
|
||||
*/
|
||||
@Singleton
|
||||
public class ExternalAuthRepository extends DepotRepository
|
||||
{
|
||||
@Inject public ExternalAuthRepository (PersistenceContext ctx)
|
||||
{
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OOOUser id of the member with the supplied external credentials, or 0 if the
|
||||
* supplied creds are not mapped to a OOOUser account.
|
||||
*/
|
||||
public int loadUserIdForExternal (ExternalAuther auther, String externalId)
|
||||
{
|
||||
Preconditions.checkNotNull(auther);
|
||||
ExternalAuthRecord exrec = load(ExternalAuthRecord.getKey(auther, externalId));
|
||||
return (exrec == null) ? 0 : exrec.userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the external id of the given OOOUser with the given ExternalAuther.
|
||||
*/
|
||||
public String loadExternalIdForUser (ExternalAuther auther, int userId)
|
||||
{
|
||||
Preconditions.checkNotNull(auther);
|
||||
ExternalAuthRecord exrec = from(ExternalAuthRecord.class).where(
|
||||
ExternalAuthRecord.USER_ID.eq(userId).and(ExternalAuthRecord.AUTHER.eq(auther))).load();
|
||||
return (exrec == null) ? null : exrec.externalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recently saved session key for the supplied external account, or null if no
|
||||
* mapping exists for said account or no key has yet been stored for it.
|
||||
*/
|
||||
public String loadExternalSessionKey (ExternalAuther auther, String externalId)
|
||||
{
|
||||
Preconditions.checkNotNull(auther);
|
||||
ExternalAuthRecord exrec = load(ExternalAuthRecord.getKey(auther, externalId));
|
||||
return (exrec == null) ? null : exrec.sessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns (externalId, sessionKey) for the supplied user, or null if no mapping exists for
|
||||
* said user. The sessionKey may be null if no key has yet been stored.
|
||||
*/
|
||||
public Tuple<String, String> loadExternalAuthInfo (ExternalAuther auther, int userId)
|
||||
{
|
||||
Preconditions.checkNotNull(auther);
|
||||
ExternalAuthRecord exrec = from(ExternalAuthRecord.class).where(
|
||||
ExternalAuthRecord.AUTHER.eq(auther), ExternalAuthRecord.USER_ID.eq(userId)).load();
|
||||
return (exrec == null) ? null : Tuple.newTuple(exrec.externalId, exrec.sessionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a mapping of (exid -> ooouser id) for all members in the supplied list that are found
|
||||
* in the database.
|
||||
*/
|
||||
public Map<String, Integer> loadUserIds (ExternalAuther auther, Collection<String> externalIds)
|
||||
{
|
||||
Preconditions.checkNotNull(auther);
|
||||
Map<String, Integer> ids = Maps.newHashMap();
|
||||
for (ExternalAuthRecord exrec : from(ExternalAuthRecord.class).where(
|
||||
ExternalAuthRecord.AUTHER.eq(auther),
|
||||
ExternalAuthRecord.EXTERNAL_ID.in(externalIds)).select()) {
|
||||
ids.put(exrec.externalId, exrec.userId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mapping for the specified user to the supplied external credentials.
|
||||
*/
|
||||
public void mapExternalAccount (int userId, ExternalAuther auther, String externalId,
|
||||
String sessionKey)
|
||||
{
|
||||
Preconditions.checkNotNull(auther);
|
||||
ExternalAuthRecord exrec = new ExternalAuthRecord();
|
||||
exrec.auther = auther;
|
||||
exrec.externalId = externalId;
|
||||
exrec.userId = userId;
|
||||
exrec.sessionKey = sessionKey;
|
||||
insert(exrec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the session key on file for the specified user's mapping for the specified auther.
|
||||
*/
|
||||
public void updateExternalSession (ExternalAuther auther, String externalId, String sessionKey)
|
||||
{
|
||||
Preconditions.checkNotNull(auther);
|
||||
updatePartial(ExternalAuthRecord.getKey(auther, externalId),
|
||||
ExternalAuthRecord.SESSION_KEY, sessionKey);
|
||||
}
|
||||
|
||||
|
||||
@Override // from DepotRepository
|
||||
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
|
||||
{
|
||||
classes.add(ExternalAuthRecord.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Retains information about a historical user registration.
|
||||
*/
|
||||
@Entity(name="history")
|
||||
public class HistoricalUserRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<HistoricalUserRecord> _R = HistoricalUserRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<String> USERNAME = colexp(_R, "username");
|
||||
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
|
||||
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The user's assigned integer userid. */
|
||||
@Id
|
||||
public int userId;
|
||||
|
||||
/** The user's chosen username. */
|
||||
@Column
|
||||
public String username;
|
||||
|
||||
/** The date this record was created. */
|
||||
@Index
|
||||
public Date created;
|
||||
|
||||
/** The affiliate site with which this user is associated. */
|
||||
@Index
|
||||
public int siteId;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link HistoricalUserRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<HistoricalUserRecord> getKey (int userId)
|
||||
{
|
||||
return newKey(_R, userId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(USER_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* An invitation record.
|
||||
*/
|
||||
@Entity
|
||||
public class InvitationRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<InvitationRecord> _R = InvitationRecord.class;
|
||||
public static final ColumnExp<String> INVITATION = colexp(_R, "invitation");
|
||||
public static final ColumnExp<String> EMAIL = colexp(_R, "email");
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<Integer> INVITER_USER_ID = colexp(_R, "inviterUserId");
|
||||
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
|
||||
public static final ColumnExp<Date> SENT = colexp(_R, "sent");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 4;
|
||||
|
||||
/** The invitation key. */
|
||||
@Id @Column(length=32)
|
||||
public String invitation;
|
||||
|
||||
/** The e-mail address this invitation was sent to. */
|
||||
public String email;
|
||||
|
||||
/** The account activated with this invitation. */
|
||||
public int userId;
|
||||
|
||||
/** The account used to generate the invitation. */
|
||||
@Index
|
||||
public int inviterUserId;
|
||||
|
||||
/** The date this invitation was created. */
|
||||
public Date created;
|
||||
|
||||
/** The date the last e-mail was sent. */
|
||||
public Date sent;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link InvitationRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<InvitationRecord> getKey (String invitation)
|
||||
{
|
||||
return newKey(_R, invitation);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(INVITATION); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.samskivert.depot.DepotRepository;
|
||||
import com.samskivert.depot.DuplicateKeyException;
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.KeySet;
|
||||
import com.samskivert.depot.Ops;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.SchemaMigration;
|
||||
import com.samskivert.depot.annotation.Computed;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.clause.FromOverride;
|
||||
import com.samskivert.depot.clause.Limit;
|
||||
import com.samskivert.depot.clause.OrderBy;
|
||||
import com.samskivert.depot.clause.Where;
|
||||
|
||||
/**
|
||||
* The invitation repository.
|
||||
*/
|
||||
@Singleton
|
||||
public class InvitationRepository extends DepotRepository
|
||||
{
|
||||
@Computed @Entity
|
||||
public static class CountRecord extends PersistentRecord
|
||||
{
|
||||
/** The computed count. */
|
||||
@Computed(fieldDefinition="count(*)")
|
||||
public int count;
|
||||
}
|
||||
|
||||
@Inject public InvitationRepository (PersistenceContext ctx)
|
||||
{
|
||||
super(ctx);
|
||||
|
||||
ctx.registerMigration(InvitationRecord.class,
|
||||
new SchemaMigration.Retype(3, InvitationRecord.INVITER_USER_ID));
|
||||
ctx.registerMigration(InvitationRecord.class,
|
||||
new SchemaMigration.Add(4, InvitationRecord.SENT, "'2000-01-01'"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new invitation record.
|
||||
*/
|
||||
public InvitationRecord createInvitationRecord (String email)
|
||||
{
|
||||
return createInvitationRecord(email, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new invitation record.
|
||||
*/
|
||||
public InvitationRecord createInvitationRecord (String email, String username)
|
||||
{
|
||||
OOOUserRecord user = load(
|
||||
OOOUserRecord.class, new Where(OOOUserRecord.USERNAME.eq(username)));
|
||||
return user == null ? null : createInvitationRecord(email, user.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new invitation record.
|
||||
*/
|
||||
public InvitationRecord createInvitationRecord (String email, int inviterId)
|
||||
{
|
||||
InvitationRecord rec = new InvitationRecord();
|
||||
rec.email = email;
|
||||
rec.inviterUserId = inviterId;
|
||||
rec.created = new Date(System.currentTimeMillis());
|
||||
rec.sent = rec.created;
|
||||
for (int ii = 0; ii < 10; ii++) {
|
||||
rec.invitation = Long.toString(Math.abs((new Random()).nextLong()), 16);
|
||||
try {
|
||||
insert(rec);
|
||||
} catch (DuplicateKeyException e) {
|
||||
continue;
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new multiple invitation record.
|
||||
*/
|
||||
public MultipleInvitationRecord createMultipleInvitationRecord (String code, int max)
|
||||
{
|
||||
MultipleInvitationRecord rec = new MultipleInvitationRecord();
|
||||
rec.invitation = code;
|
||||
rec.maxInvitations = max;
|
||||
try {
|
||||
insert(rec);
|
||||
} catch (DuplicateKeyException e) {
|
||||
return null;
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an invitation record.
|
||||
*/
|
||||
public InvitationRecord getInvitation (String invitation)
|
||||
{
|
||||
return load(InvitationRecord.getKey(invitation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of invitations starting with the specified record number and containing at
|
||||
* most <code>count</code> elements.
|
||||
*/
|
||||
public List<InvitationRecord> getInvitationRecords (int start, int count)
|
||||
{
|
||||
return findAll(InvitationRecord.class, OrderBy.descending(InvitationRecord.CREATED),
|
||||
new Limit(start, count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of unused invitations created before a certain date.
|
||||
*/
|
||||
public List<InvitationRecord> getUnusedInvitationRecords (Date before)
|
||||
{
|
||||
return findAll(InvitationRecord.class,
|
||||
new Where(Ops.and(InvitationRecord.USER_ID.eq(0),
|
||||
InvitationRecord.CREATED.lessEq(before),
|
||||
InvitationRecord.SENT.lessEq(before))),
|
||||
OrderBy.ascending(InvitationRecord.EMAIL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the multiple invitation code is valid and has invitations remaining.
|
||||
*/
|
||||
public MultipleInvitationRecord getOpenMultipleInvitation (String code)
|
||||
{
|
||||
MultipleInvitationRecord rec = load(MultipleInvitationRecord.class,
|
||||
new Where(MultipleInvitationRecord.INVITATION.eq(code)));
|
||||
if (rec == null) {
|
||||
return null;
|
||||
}
|
||||
return isMultipleInvitationOpen(rec) ? rec : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of multiple invtations records.
|
||||
*/
|
||||
public List<MultipleInvitationRecord> getMultipleInvitations ()
|
||||
{
|
||||
return findAll(MultipleInvitationRecord.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates an invitation record.
|
||||
*/
|
||||
public void activateInvitation (InvitationRecord rec, int userId)
|
||||
{
|
||||
rec.userId = userId;
|
||||
update(rec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates a multiple invitation.
|
||||
*
|
||||
* @return false if the user has already claimed an invitation
|
||||
*/
|
||||
public boolean activateMultipleInvitation (int invitationId, int userId)
|
||||
{
|
||||
UserInvitationRecord uir = new UserInvitationRecord();
|
||||
uir.invitationId = invitationId;
|
||||
uir.userId = userId;
|
||||
try {
|
||||
insert(uir);
|
||||
} catch (DuplicateKeyException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the created date for a set of invitations.
|
||||
*/
|
||||
public void pokeInvitations (List<String> invitations)
|
||||
{
|
||||
updatePartial(InvitationRecord.class,
|
||||
new Where(InvitationRecord.INVITATION.in(invitations)),
|
||||
KeySet.newKeySet(InvitationRecord.class, Lists.transform(invitations,
|
||||
new Function<String, Key<InvitationRecord>>() {
|
||||
public Key<InvitationRecord> apply (String invitation) {
|
||||
return InvitationRecord.getKey(invitation);
|
||||
}
|
||||
})),
|
||||
InvitationRecord.SENT, new Date(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a multiple invitation record is still open.
|
||||
*/
|
||||
protected boolean isMultipleInvitationOpen (MultipleInvitationRecord rec)
|
||||
{
|
||||
return (rec.maxInvitations > load(CountRecord.class,
|
||||
new FromOverride(UserInvitationRecord.class),
|
||||
new Where(UserInvitationRecord.INVITATION_ID.eq(rec.invitationId))).count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
|
||||
{
|
||||
classes.add(InvitationRecord.class);
|
||||
classes.add(MultipleInvitationRecord.class);
|
||||
classes.add(UserInvitationRecord.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.annotation.Computed;
|
||||
|
||||
/**
|
||||
* Used to determine the max userId.
|
||||
*/
|
||||
@Computed
|
||||
public class MaxUserRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<MaxUserRecord> _R = MaxUserRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** The user id. */
|
||||
@Computed
|
||||
public int userId;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.GenerationType;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* A multiple invitation record.
|
||||
*/
|
||||
@Entity
|
||||
public class MultipleInvitationRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<MultipleInvitationRecord> _R = MultipleInvitationRecord.class;
|
||||
public static final ColumnExp<Integer> INVITATION_ID = colexp(_R, "invitationId");
|
||||
public static final ColumnExp<String> INVITATION = colexp(_R, "invitation");
|
||||
public static final ColumnExp<Integer> MAX_INVITATIONS = colexp(_R, "maxInvitations");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The invitation id. */
|
||||
@Id @GeneratedValue(strategy=GenerationType.AUTO)
|
||||
public int invitationId;
|
||||
|
||||
/** The invitation code. */
|
||||
@Column(length=32, unique=true)
|
||||
public String invitation;
|
||||
|
||||
/** The max number of accounts that can be created with this invitation. */
|
||||
public int maxInvitations;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link MultipleInvitationRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<MultipleInvitationRecord> getKey (int invitationId)
|
||||
{
|
||||
return newKey(_R, invitationId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(INVITATION_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
import com.threerings.user.OOOAuxData;
|
||||
|
||||
/**
|
||||
* Emulates {@link OOOAuxData} for the Depot.
|
||||
*/
|
||||
@Entity(name="AUXDATA")
|
||||
public class OOOAuxDataRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<OOOAuxDataRecord> _R = OOOAuxDataRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<Date> BIRTHDAY = colexp(_R, "birthday");
|
||||
public static final ColumnExp<Byte> GENDER = colexp(_R, "gender");
|
||||
public static final ColumnExp<String> MISSIVE = colexp(_R, "missive");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The user's unique identifier. */
|
||||
@Id @Column(name="USER_ID")
|
||||
public int userId;
|
||||
|
||||
/** The user's birthday. */
|
||||
@Column(name="BIRTHDAY", defaultValue="'1900-01-01'")
|
||||
public Date birthday;
|
||||
|
||||
/** The user's gender. */
|
||||
@Column(name="GENDER", defaultValue="0")
|
||||
public byte gender;
|
||||
|
||||
/** The user's personal missive to us. */
|
||||
@Column(name="MISSIVE")
|
||||
public String missive;
|
||||
|
||||
/**
|
||||
* Creates a OOOAuxDataRecord from a OOOAuxData.
|
||||
*/
|
||||
public static OOOAuxDataRecord fromOOOAuxData (OOOAuxData aux)
|
||||
{
|
||||
OOOAuxDataRecord record = new OOOAuxDataRecord();
|
||||
record.userId = aux.userId;
|
||||
record.birthday = aux.birthday;
|
||||
record.gender = aux.gender;
|
||||
record.missive = aux.missive;
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a OOOAuxData version of this record.
|
||||
*/
|
||||
public OOOAuxData toOOOAuxData ()
|
||||
{
|
||||
OOOAuxData aux = new OOOAuxData();
|
||||
aux.userId = userId;
|
||||
aux.birthday = birthday;
|
||||
aux.gender = gender;
|
||||
aux.missive = missive;
|
||||
return aux;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link OOOAuxDataRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<OOOAuxDataRecord> getKey (int userId)
|
||||
{
|
||||
return newKey(_R, userId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(USER_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
import com.threerings.user.OOOBillAuxData;
|
||||
|
||||
/**
|
||||
* Emulates {@link OOOBillAuxData} for the Depot.
|
||||
*/
|
||||
@Entity(name="BILLAUXDATA")
|
||||
public class OOOBillAuxDataRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<OOOBillAuxDataRecord> _R = OOOBillAuxDataRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<Timestamp> FIRST_COIN_BUY = colexp(_R, "firstCoinBuy");
|
||||
public static final ColumnExp<Timestamp> LATEST_COIN_BUY = colexp(_R, "latestCoinBuy");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 2;
|
||||
|
||||
/** The user's unique identifier. */
|
||||
@Id @Column(name="USER_ID")
|
||||
public int userId;
|
||||
|
||||
/** The first time the user bought coins. */
|
||||
@Column(name="FIRST_COIN_BUY", nullable=true, defaultValue="NULL")
|
||||
public Timestamp firstCoinBuy;
|
||||
|
||||
/** The most recent time the user bought coins. */
|
||||
@Column(name="LATEST_COIN_BUY", nullable=true, defaultValue="NULL")
|
||||
public Timestamp latestCoinBuy;
|
||||
|
||||
/**
|
||||
* Creates a OOOBillAuxDataRecord from OOOBillAuxData.
|
||||
*/
|
||||
public static OOOBillAuxDataRecord fromOOOBillAuxData (OOOBillAuxData aux)
|
||||
{
|
||||
OOOBillAuxDataRecord record = new OOOBillAuxDataRecord();
|
||||
record.userId = aux.userId;
|
||||
record.firstCoinBuy = aux.firstCoinBuy;
|
||||
record.latestCoinBuy = aux.latestCoinBuy;
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a OOOBillAuxData version of this record.
|
||||
*/
|
||||
public OOOBillAuxData toOOOBillAuxData ()
|
||||
{
|
||||
OOOBillAuxData aux = new OOOBillAuxData();
|
||||
aux.userId = userId;
|
||||
aux.firstCoinBuy = firstCoinBuy;
|
||||
aux.latestCoinBuy = latestCoinBuy;
|
||||
return aux;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link OOOBillAuxDataRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<OOOBillAuxDataRecord> getKey (int userId)
|
||||
{
|
||||
return newKey(_R, userId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(USER_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.StringFuncs;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.GenerationType;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.clause.OrderBy.Order;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.expression.SQLExpression;
|
||||
|
||||
import com.samskivert.util.Tuple;
|
||||
|
||||
import com.threerings.user.OOOUser;
|
||||
|
||||
/**
|
||||
* Emulates {@link OOOUser} for the Depot.
|
||||
*/
|
||||
@Entity(name="users",
|
||||
indices={ @Index(name="ixLowerUsername", unique=true), @Index(name="ixLowerEmail") })
|
||||
public class OOOUserRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<OOOUserRecord> _R = OOOUserRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<String> USERNAME = colexp(_R, "username");
|
||||
public static final ColumnExp<String> PASSWORD = colexp(_R, "password");
|
||||
public static final ColumnExp<String> EMAIL = colexp(_R, "email");
|
||||
public static final ColumnExp<String> REALNAME = colexp(_R, "realname");
|
||||
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
|
||||
public static final ColumnExp<Integer> SITE_ID = colexp(_R, "siteId");
|
||||
public static final ColumnExp<Integer> AFFILIATE_TAG_ID = colexp(_R, "affiliateTagId");
|
||||
public static final ColumnExp<Integer> FLAGS = colexp(_R, "flags");
|
||||
public static final ColumnExp<byte[]> TOKENS = colexp(_R, "tokens");
|
||||
public static final ColumnExp<Byte> YOHOHO = colexp(_R, "yohoho");
|
||||
public static final ColumnExp<String> SPOTS = colexp(_R, "spots");
|
||||
public static final ColumnExp<Integer> SHUN_LEFT = colexp(_R, "shunLeft");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** Fields that will be checked for modification and updated when calling
|
||||
* {@link DepotUserRepository#updateUser}. */
|
||||
public static ColumnExp<?>[] UPDATABLE_FIELDS = {
|
||||
USERNAME, PASSWORD, EMAIL, REALNAME, AFFILIATE_TAG_ID,
|
||||
FLAGS, TOKENS, YOHOHO, SPOTS, SHUN_LEFT
|
||||
};
|
||||
|
||||
public static final int SCHEMA_VERSION = 4;
|
||||
|
||||
/** The user's assigned integer userid. */
|
||||
@Id @GeneratedValue(strategy=GenerationType.AUTO)
|
||||
public int userId;
|
||||
|
||||
/** The user's chosen username. */
|
||||
@Column(length=128, unique=true)
|
||||
public String username;
|
||||
|
||||
/** The user's chosen password (encrypted). */
|
||||
@Column(length=128)
|
||||
public String password;
|
||||
|
||||
/** The user's email address. */
|
||||
@Column(length=128) @Index(name="ixEmail")
|
||||
public String email;
|
||||
|
||||
/** The user's real name (first, last and whatever else they opt to provide). */
|
||||
@Column(length=128)
|
||||
public String realname;
|
||||
|
||||
/** The date this record was created. */
|
||||
public Date created;
|
||||
|
||||
/** The site identifier of the site through which the user created
|
||||
* their account. (Their affiliation, if you will.) */
|
||||
public int siteId;
|
||||
|
||||
/** The id of any opaque tag provided by the affiliate to tag this user for their purposes. */
|
||||
public int affiliateTagId;
|
||||
|
||||
/** The flags detailing the user's various bits of status. (VALIDATED_FLAG, etc) */
|
||||
public int flags;
|
||||
|
||||
/** The tokens detailing the user's site access permissions. (ADMIN, TESTER, etc) */
|
||||
public byte[] tokens;
|
||||
|
||||
/** The user's account status for Puzzle Pirates. (TRIAL_STATE, SUBSCRIBER_STATE, etc) */
|
||||
public byte yohoho;
|
||||
|
||||
/** The spots that have been given to the user by various crews. */
|
||||
@Column(length=128)
|
||||
public String spots;
|
||||
|
||||
/** The amount of time remaining on the users shun, in minutes. */
|
||||
public int shunLeft;
|
||||
|
||||
/**
|
||||
* Defines the index on {@link #username} converted to lower case.
|
||||
*/
|
||||
public static Tuple<SQLExpression<?>, Order> ixLowerUsername ()
|
||||
{
|
||||
return new Tuple<SQLExpression<?>, Order>(
|
||||
StringFuncs.lower(OOOUserRecord.USERNAME), Order.ASC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the index on {@link #email} converted to lower case.
|
||||
*/
|
||||
public static Tuple<SQLExpression<?>, Order> ixLowerEmail ()
|
||||
{
|
||||
return new Tuple<SQLExpression<?>, Order>(StringFuncs.lower(OOOUserRecord.EMAIL), Order.ASC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a OOOUserRecord from a OOOUser.
|
||||
*/
|
||||
public static OOOUserRecord fromUser (OOOUser user)
|
||||
{
|
||||
OOOUserRecord record = new OOOUserRecord();
|
||||
record.userId = user.userId;
|
||||
record.username = user.username;
|
||||
record.created = user.created;
|
||||
record.realname = user.realname;
|
||||
record.password = user.password;
|
||||
record.email = user.email;
|
||||
record.siteId = user.siteId;
|
||||
record.flags = user.flags;
|
||||
record.tokens = user.tokens;
|
||||
record.yohoho = user.yohoho;
|
||||
record.spots = user.spots;
|
||||
record.shunLeft = user.shunLeft;
|
||||
record.affiliateTagId = user.affiliateTagId;
|
||||
return record;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link OOOUserRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<OOOUserRecord> getKey (int userId)
|
||||
{
|
||||
return newKey(_R, userId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(USER_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
|
||||
/**
|
||||
* Returns true if this user holds the specified token.
|
||||
*/
|
||||
public boolean holdsToken (byte token)
|
||||
{
|
||||
if (tokens == null) {
|
||||
return false;
|
||||
}
|
||||
for (byte heldTok : tokens) {
|
||||
if (heldTok == token) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a OOOUser version of this record.
|
||||
*/
|
||||
public OOOUser toUser ()
|
||||
{
|
||||
OOOUser user = new DepotOOOUser();
|
||||
user.userId = userId;
|
||||
user.username = username;
|
||||
user.created = created;
|
||||
user.realname = realname;
|
||||
user.password = password;
|
||||
user.email = email;
|
||||
user.siteId = siteId;
|
||||
user.flags = flags;
|
||||
user.tokens = tokens;
|
||||
user.yohoho = yohoho;
|
||||
user.spots = spots;
|
||||
user.shunLeft = shunLeft;
|
||||
user.affiliateTagId = affiliateTagId;
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* A OOOUser with special dirty handling.
|
||||
*/
|
||||
protected class DepotOOOUser extends OOOUser
|
||||
{
|
||||
public Set<ColumnExp<?>> mods;
|
||||
|
||||
@Override
|
||||
protected void setModified (String field)
|
||||
{
|
||||
if (mods == null) {
|
||||
mods = Sets.newHashSet();
|
||||
}
|
||||
mods.add(colexp(_R, field));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Actions that have been processed by a server.
|
||||
*/
|
||||
@Entity(name="PROCESSED_ACTIONS")
|
||||
public class ProcessedActionRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<ProcessedActionRecord> _R = ProcessedActionRecord.class;
|
||||
public static final ColumnExp<Integer> ACTION_ID = colexp(_R, "actionId");
|
||||
public static final ColumnExp<String> SERVER = colexp(_R, "server");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The action id. */
|
||||
@Id @Column(name="ACTION_ID")
|
||||
public int actionId;
|
||||
|
||||
/** The name of the server that has processed the action. */
|
||||
@Id @Column(name="SERVER")
|
||||
public String server;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link ProcessedActionRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<ProcessedActionRecord> getKey (int actionId, String server)
|
||||
{
|
||||
return newKey(_R, actionId, server);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(ACTION_ID, SERVER); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.annotation.Computed;
|
||||
|
||||
/**
|
||||
* Used to summarize recent registrants from the {@link HistoricalUserRecord} table.
|
||||
*/
|
||||
@Computed(shadowOf=HistoricalUserRecord.class)
|
||||
public class RecentUserRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<RecentUserRecord> _R = RecentUserRecord.class;
|
||||
public static final ColumnExp<Date> CREATED = colexp(_R, "created");
|
||||
public static final ColumnExp<Integer> ENTRIES = colexp(_R, "entries");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** The created date. */
|
||||
public Date created;
|
||||
|
||||
/** The number of users created on the date. */
|
||||
@Computed(fieldDefinition="count(*)")
|
||||
public int entries;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.GenerationType;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Records information on a particular referral.
|
||||
*/
|
||||
@Entity(name="REFERRAL")
|
||||
public class ReferralRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<ReferralRecord> _R = ReferralRecord.class;
|
||||
public static final ColumnExp<Integer> REFERRAL_ID = colexp(_R, "referralId");
|
||||
public static final ColumnExp<Date> RECORDED = colexp(_R, "recorded");
|
||||
public static final ColumnExp<Integer> REFERRER_ID = colexp(_R, "referrerId");
|
||||
public static final ColumnExp<String> DATA = colexp(_R, "data");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 2;
|
||||
|
||||
/** The refferal's assigned integer id. */
|
||||
@Id @GeneratedValue(strategy=GenerationType.AUTO) @Column(name="REFERRAL_ID")
|
||||
public int referralId;
|
||||
|
||||
/** The date the referral was made. */
|
||||
@Column(name="RECORDED", defaultValue="'1900-01-01'") @Index
|
||||
public Date recorded;
|
||||
|
||||
/** The user id of the referring user. */
|
||||
@Column(name="REFERRER_ID", defaultValue="0") @Index
|
||||
public int referrerId;
|
||||
|
||||
/** Data associated with this referral record. */
|
||||
@Column(name="DATA")
|
||||
public String data;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link ReferralRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<ReferralRecord> getKey (int referralId)
|
||||
{
|
||||
return newKey(_R, referralId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(REFERRAL_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.samskivert.depot.DepotRepository;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.clause.Where;
|
||||
import com.samskivert.util.Calendars;
|
||||
|
||||
/**
|
||||
* Tracks the data needed for our personal referral system whereby one
|
||||
* user sends a referral link (or email) to their friend and we track the
|
||||
* original referring user if their referred friend actually registers
|
||||
* within some reasonable time frame (like a couple of weeks).
|
||||
*/
|
||||
@Singleton
|
||||
public class ReferralRepository extends DepotRepository
|
||||
{
|
||||
@Inject public ReferralRepository (PersistenceContext ctx)
|
||||
{
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a new referral record in the system and returns the unique
|
||||
* identifier for said record.
|
||||
*/
|
||||
public int recordReferral (int referrerId, String data)
|
||||
{
|
||||
final ReferralRecord record = new ReferralRecord();
|
||||
record.recorded = new Date(System.currentTimeMillis());
|
||||
record.referrerId = referrerId;
|
||||
record.data = data;
|
||||
insert(record);
|
||||
|
||||
checkPurge();
|
||||
return record.referralId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a referral record with the specified id, returning null if
|
||||
* no matching record could be found.
|
||||
*/
|
||||
public ReferralRecord lookupReferral (int referralId)
|
||||
{
|
||||
return load(ReferralRecord.getKey(referralId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a referral record with the specified referring user id,
|
||||
* returning null if no matching record could be found.
|
||||
*/
|
||||
public ReferralRecord lookupReferrer (int referrerId)
|
||||
{
|
||||
return load(ReferralRecord.class, new Where(ReferralRecord.REFERRER_ID, referrerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to periodically purge old referral records from the
|
||||
* repository.
|
||||
*/
|
||||
protected void checkPurge ()
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - _lastPurge > PURGE_INTERVAL) {
|
||||
_lastPurge = now;
|
||||
purgeStaleReferrals();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges stale referral records from the repository.
|
||||
*/
|
||||
protected void purgeStaleReferrals ()
|
||||
{
|
||||
deleteAll(ReferralRecord.class, new Where(ReferralRecord.RECORDED.lessThan(
|
||||
Calendars.now().zeroTime().addMonths(-1).toSQLDate())));
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
|
||||
{
|
||||
classes.add(ReferralRecord.class);
|
||||
}
|
||||
|
||||
/** The last time we purged the repository of stale records. */
|
||||
protected long _lastPurge;
|
||||
|
||||
/** We purge the repository every three hours of stale records. */
|
||||
protected static final long PURGE_INTERVAL = 3 * 60 * 60 * 1000L;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.annotation.Computed;
|
||||
|
||||
/**
|
||||
* Used to summarize rewards.
|
||||
*/
|
||||
@Computed(shadowOf=RewardInfoRecord.class)
|
||||
public class RewardCountRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<RewardCountRecord> _R = RewardCountRecord.class;
|
||||
public static final ColumnExp<Integer> REWARD_ID = colexp(_R, "rewardId");
|
||||
public static final ColumnExp<Integer> COUNT = colexp(_R, "count");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
/** The reward id. */
|
||||
public int rewardId;
|
||||
|
||||
/** The number of matching records. */
|
||||
@Computed(fieldDefinition="count(*)")
|
||||
public int count;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.GeneratedValue;
|
||||
import com.samskivert.depot.annotation.GenerationType;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
/**
|
||||
* A reward being offered.
|
||||
*/
|
||||
@Entity(name="REWARD_INFO")
|
||||
public class RewardInfoRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<RewardInfoRecord> _R = RewardInfoRecord.class;
|
||||
public static final ColumnExp<Integer> REWARD_ID = colexp(_R, "rewardId");
|
||||
public static final ColumnExp<String> DESCRIPTION = colexp(_R, "description");
|
||||
public static final ColumnExp<String> COUPON_CODE = colexp(_R, "couponCode");
|
||||
public static final ColumnExp<String> DATA = colexp(_R, "data");
|
||||
public static final ColumnExp<Date> EXPIRATION = colexp(_R, "expiration");
|
||||
public static final ColumnExp<Integer> MAX_ELIGIBLE_ID = colexp(_R, "maxEligibleId");
|
||||
public static final ColumnExp<Integer> ACTIVATIONS = colexp(_R, "activations");
|
||||
public static final ColumnExp<Integer> REDEMPTIONS = colexp(_R, "redemptions");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** Unique reward id. */
|
||||
@Id @Column(name="REWARD_ID") @GeneratedValue(strategy=GenerationType.AUTO)
|
||||
public int rewardId;
|
||||
|
||||
/** Reward description. */
|
||||
@Column(name="DESCRIPTION")
|
||||
public String description;
|
||||
|
||||
/** A coupon code required to activate this reward or null. */
|
||||
@Column(name="COUPON_CODE", nullable=true)
|
||||
public String couponCode;
|
||||
|
||||
/** Game specific reward data. */
|
||||
@Column(name="DATA")
|
||||
public String data;
|
||||
|
||||
/** Date when the reward expires. */
|
||||
@Column(name="EXPIRATION")
|
||||
public Date expiration;
|
||||
|
||||
/** Max userid that can redeem this reward. */
|
||||
@Column(name="MAX_ELIGIBLE_ID")
|
||||
public int maxEligibleId;
|
||||
|
||||
/** Number of activated rewards. */
|
||||
@Column(name="ACTIVATIONS")
|
||||
public int activations;
|
||||
|
||||
/** Number of claimed rewards. */
|
||||
@Column(name="REDEMPTIONS")
|
||||
public int redemptions;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link RewardInfoRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<RewardInfoRecord> getKey (int rewardId)
|
||||
{
|
||||
return newKey(_R, rewardId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(REWARD_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
|
||||
@Override // from Object
|
||||
public String toString ()
|
||||
{
|
||||
return StringUtil.fieldsToString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* A persistent reward.
|
||||
*/
|
||||
@Entity(name="REWARD_RECORDS")
|
||||
public class RewardRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<RewardRecord> _R = RewardRecord.class;
|
||||
public static final ColumnExp<Integer> REWARD_ID = colexp(_R, "rewardId");
|
||||
public static final ColumnExp<String> ACCOUNT = colexp(_R, "account");
|
||||
public static final ColumnExp<String> REDEEMER_IDENT = colexp(_R, "redeemerIdent");
|
||||
public static final ColumnExp<String> PARAM = colexp(_R, "param");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** RewardInfo reward id. */
|
||||
@Id @Column(name="REWARD_ID")
|
||||
public int rewardId;
|
||||
|
||||
/** Account name. */
|
||||
@Id @Index(name="ixAccount") @Column(name="ACCOUNT", nullable=true)
|
||||
public String account;
|
||||
|
||||
/** Unique identification value. */
|
||||
@Index(name="ixRedeemerIdent") @Column(name="REDEEMER_IDENT", length=64, nullable=true)
|
||||
public String redeemerIdent;
|
||||
|
||||
/**
|
||||
* A generic parameter string that can tell the reward handler more specifically when and what
|
||||
* item, etc. to hand out for this instance of the reward.
|
||||
*/
|
||||
@Id @Column(name="PARAM", nullable=true)
|
||||
public String param;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link RewardRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<RewardRecord> getKey (int rewardId, String account, String param)
|
||||
{
|
||||
return newKey(_R, rewardId, account, param);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(REWARD_ID, ACCOUNT, PARAM); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import com.samskivert.depot.DepotRepository;
|
||||
import com.samskivert.depot.DuplicateKeyException;
|
||||
import com.samskivert.depot.KeySet;
|
||||
import com.samskivert.depot.Ops;
|
||||
import com.samskivert.depot.PersistenceContext;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.clause.FromOverride;
|
||||
import com.samskivert.depot.clause.GroupBy;
|
||||
import com.samskivert.depot.clause.OrderBy;
|
||||
import com.samskivert.depot.clause.Where;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.expression.SQLExpression;
|
||||
import com.samskivert.jdbc.ConnectionProvider;
|
||||
|
||||
import com.threerings.user.OOOUserRepository;
|
||||
|
||||
import static com.threerings.user.Log.log;
|
||||
|
||||
/**
|
||||
* Maintains persistent reward information.
|
||||
*/
|
||||
@Singleton
|
||||
public class RewardRepository extends DepotRepository
|
||||
{
|
||||
@Inject public RewardRepository (PersistenceContext ctx)
|
||||
{
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the repository with the specified connection provider.
|
||||
*
|
||||
*/
|
||||
public RewardRepository (ConnectionProvider provider)
|
||||
{
|
||||
super(new PersistenceContext(OOOUserRepository.USER_REPOSITORY_IDENT, provider, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RewardInfo record in the database. <code>info</code> should have the
|
||||
* <code>description</code>, <code>data</code> and <code>expiration</code> filled in.
|
||||
*/
|
||||
public void createReward (RewardInfoRecord info)
|
||||
{
|
||||
//info.maxEligibleId = getMaxUserId();
|
||||
store(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately expires a reward by setting the expiration to the current date then making a
|
||||
* call to <code>purgeExpiredRewards</code>.
|
||||
*/
|
||||
public void expireReward (int rewardId)
|
||||
{
|
||||
updatePartial(RewardInfoRecord.getKey(rewardId), RewardInfoRecord.EXPIRATION,
|
||||
new Date(System.currentTimeMillis()));
|
||||
purgeExpiredRewards();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to activate the reward for an account. If this account has not already activated
|
||||
* the reward, a new RewardRecord will be created. Returns true if a new RewardRecord is
|
||||
* created, false otherwise.
|
||||
*/
|
||||
public boolean activateReward (int rewardId, String account)
|
||||
{
|
||||
return activateReward(rewardId, account, null);
|
||||
}
|
||||
|
||||
public boolean activateReward (int rewardId, String account, String param)
|
||||
{
|
||||
RewardRecord rr = new RewardRecord();
|
||||
rr.rewardId = rewardId;
|
||||
rr.account = account;
|
||||
rr.param = param == null ? "" : param;
|
||||
try {
|
||||
insert(rr);
|
||||
} catch (DuplicateKeyException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates monthly rewards of the specified ID for the account between start and end. If they
|
||||
* already have monthly rewards for this ID, will add new ones only at appropriate monthly
|
||||
* intervals from the existing ones.
|
||||
*/
|
||||
public boolean activateMonthlyRewards (
|
||||
int rewardId, String account, java.util.Date start, java.util.Date end)
|
||||
{
|
||||
// Find the most recent reward this account already has of this type.
|
||||
java.util.Date latest = null;
|
||||
List<RewardRecord> matches = findAll(RewardRecord.class,
|
||||
new Where(Ops.and(RewardRecord.REWARD_ID.eq(rewardId),
|
||||
RewardRecord.ACCOUNT.eq(account))));
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// Find the most recent reward this account already has of this type.
|
||||
for (RewardRecord rec : matches) {
|
||||
if (rec.param == null) {
|
||||
log.warning("Missing monthly reward param: ", "rewardId", rewardId,
|
||||
"account", account);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
java.util.Date thisDate = dateFormat.parse(rec.param);
|
||||
if (latest == null || thisDate.after(latest)) {
|
||||
latest = thisDate;
|
||||
}
|
||||
} catch (ParseException pe) {
|
||||
log.warning("Bogus reward param: ", "rewardId", rewardId,
|
||||
"account", account, "param", rec.param);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// We use the maximum of our provided start time and a month-after-latest
|
||||
// as the first time we'll input a reward. Note that this can result
|
||||
// in no rewards being given.
|
||||
Calendar startCal = Calendar.getInstance();
|
||||
if (latest != null) {
|
||||
startCal.setTime(latest);
|
||||
startCal.add(Calendar.MONTH, 1);
|
||||
if (startCal.getTime().before(start)) {
|
||||
startCal.setTime(start);
|
||||
}
|
||||
} else {
|
||||
startCal.setTime(start);
|
||||
}
|
||||
|
||||
// Add in any appropriate rewards between the modified start and end.
|
||||
while (startCal.getTime().before(end)) {
|
||||
RewardRecord rr = new RewardRecord();
|
||||
rr.rewardId = rewardId;
|
||||
rr.account = account;
|
||||
rr.param = dateFormat.format(startCal.getTime());
|
||||
insert(rr);
|
||||
startCal.add(Calendar.MONTH, 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates all rewards of the specified ID and account during the time window.
|
||||
* Returns the number of rewards deleted.
|
||||
*/
|
||||
public int deactivateMonthlyRewards (
|
||||
int rewardId, String account, java.util.Date start, java.util.Date end)
|
||||
{
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
String startStr = dateFormat.format(start);
|
||||
String endStr = dateFormat.format(end);
|
||||
|
||||
return deleteAll(RewardRecord.class,
|
||||
new Where(Ops.and(
|
||||
RewardRecord.REWARD_ID.eq(rewardId),
|
||||
RewardRecord.ACCOUNT.eq(account),
|
||||
RewardRecord.PARAM.greaterEq(startStr),
|
||||
RewardRecord.PARAM.lessEq(endStr),
|
||||
RewardRecord.REDEEMER_IDENT.isNull())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all currently active rewards.
|
||||
*/
|
||||
public List<RewardInfoRecord> loadActiveRewards ()
|
||||
{
|
||||
return findAll(RewardInfoRecord.class,
|
||||
new Where(RewardInfoRecord.EXPIRATION.greaterThan(
|
||||
new Timestamp(System.currentTimeMillis()))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all rewards.
|
||||
*/
|
||||
public List<RewardInfoRecord> loadRewards ()
|
||||
{
|
||||
return findAll(RewardInfoRecord.class, OrderBy.descending(RewardInfoRecord.REWARD_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s that match the given account name and whose eligible
|
||||
* date is in the past. The returned list will be sorted by <code>rewardId</code>.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedRewards (String account)
|
||||
{
|
||||
return findAll(RewardRecord.class,
|
||||
new Where(RewardRecord.ACCOUNT.eq(account)),
|
||||
OrderBy.ascending(RewardRecord.REWARD_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s that match either the account or redeemer identifier.
|
||||
* The returned list will be sorted by <code>rewardId</code>.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedRewards (String account, String redeemerIdent)
|
||||
{
|
||||
return findAll(RewardRecord.class,
|
||||
new Where(Ops.or(RewardRecord.ACCOUNT.eq(account),
|
||||
RewardRecord.REDEEMER_IDENT.eq(redeemerIdent))),
|
||||
OrderBy.ascending(RewardRecord.REWARD_ID).thenAscending(RewardRecord.PARAM));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> that match
|
||||
* either the account or redeemer identifier.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedReward (
|
||||
String account, String redeemerIdent, int rewardId)
|
||||
{
|
||||
return findAll(RewardRecord.class,
|
||||
new Where(Ops.and(RewardRecord.REWARD_ID.eq(rewardId),
|
||||
Ops.or(RewardRecord.ACCOUNT.eq(account),
|
||||
RewardRecord.REDEEMER_IDENT.eq(redeemerIdent)))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all <code>RewardRecord</code>s for a specified <code>rewardId</code> and
|
||||
* <code>param</code> that match either the account or redeemer identifier.
|
||||
*/
|
||||
public List<RewardRecord> loadActivatedReward (
|
||||
String account, String redeemerIdent, int rewardId, String param)
|
||||
{
|
||||
SQLExpression<?> paramExp = param == null ?
|
||||
RewardRecord.PARAM.isNull() : RewardRecord.PARAM.eq(param);
|
||||
return findAll(RewardRecord.class,
|
||||
new Where(Ops.and(
|
||||
RewardRecord.REWARD_ID.eq(rewardId),
|
||||
paramExp,
|
||||
Ops.or(
|
||||
RewardRecord.ACCOUNT.eq(account),
|
||||
RewardRecord.REDEEMER_IDENT.eq(redeemerIdent)))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the supplied RewardRecord with the indicated <code>redeemerIdent</code> and store it
|
||||
* to the database.
|
||||
*/
|
||||
public void redeemReward (RewardRecord record, String redeemerIdent)
|
||||
{
|
||||
record.redeemerIdent = redeemerIdent;
|
||||
update(record, RewardRecord.REDEEMER_IDENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarizes the reward data and purges expired rewards.
|
||||
*/
|
||||
public void purgeExpiredRewards ()
|
||||
{
|
||||
summarizeAndUpdate(null, RewardInfoRecord.ACTIVATIONS);
|
||||
summarizeAndUpdate(
|
||||
Ops.not(RewardRecord.REDEEMER_IDENT.isNull()), RewardInfoRecord.REDEMPTIONS);
|
||||
deleteAll(RewardRecord.class,
|
||||
KeySet.newKeySet(RewardRecord.class, findAllKeys(RewardRecord.class, true,
|
||||
new FromOverride(RewardRecord.class, RewardInfoRecord.class),
|
||||
new Where(Ops.and(
|
||||
RewardRecord.REWARD_ID.eq(RewardInfoRecord.REWARD_ID),
|
||||
RewardInfoRecord.EXPIRATION.lessEq(new Timestamp(System.currentTimeMillis()))
|
||||
)))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarizes the reward data and stores it in the reward info records.
|
||||
*/
|
||||
protected void summarizeAndUpdate (SQLExpression<?> condition, ColumnExp<?> column)
|
||||
{
|
||||
SQLExpression<?> where = RewardInfoRecord.REWARD_ID.eq(RewardRecord.REWARD_ID);
|
||||
if (condition != null) {
|
||||
where = Ops.and(where, condition);
|
||||
}
|
||||
List<RewardCountRecord> counts = findAll(RewardCountRecord.class,
|
||||
new FromOverride(RewardInfoRecord.class, RewardRecord.class),
|
||||
new Where(where),
|
||||
new GroupBy(RewardInfoRecord.REWARD_ID));
|
||||
for (RewardCountRecord record : counts) {
|
||||
updatePartial(RewardInfoRecord.getKey(record.rewardId), column, record.count);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
|
||||
{
|
||||
classes.add(RewardRecord.class);
|
||||
classes.add(RewardInfoRecord.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
|
||||
/**
|
||||
* Maintains information on authenticated HTTP sessions.
|
||||
*/
|
||||
@Entity(name="sessions")
|
||||
public class SessionRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<SessionRecord> _R = SessionRecord.class;
|
||||
public static final ColumnExp<String> AUTHCODE = colexp(_R, "authcode");
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<Date> EXPIRES = colexp(_R, "expires");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** A unique code that identifies this session. */
|
||||
@Id @Column(length=32)
|
||||
public String authcode;
|
||||
|
||||
/** The id of the user authenticated in this session. */
|
||||
@Index(name="userid_index")
|
||||
public int userId;
|
||||
|
||||
/** The date on which the session expires. */
|
||||
@Index(name="expires_index")
|
||||
public Date expires;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link SessionRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<SessionRecord> getKey (String authcode)
|
||||
{
|
||||
return newKey(_R, authcode);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(AUTHCODE); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Represents a row in the TAINTED_IDENTS table, Depot version.
|
||||
*/
|
||||
@Entity(name="TAINTED_IDENTS")
|
||||
public class TaintedIdentRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<TaintedIdentRecord> _R = TaintedIdentRecord.class;
|
||||
public static final ColumnExp<String> MACH_IDENT = colexp(_R, "machIdent");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** A 'unique' id for a specific machine we have seen. */
|
||||
@Id @Column(name="MACH_IDENT")
|
||||
public String machIdent;
|
||||
|
||||
/** Blank constructor for the unserialization business. */
|
||||
public TaintedIdentRecord ()
|
||||
{
|
||||
}
|
||||
|
||||
/** A constructor that populates this record. */
|
||||
public TaintedIdentRecord (String machIdent)
|
||||
{
|
||||
this.machIdent = machIdent;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link TaintedIdentRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<TaintedIdentRecord> getKey (String machIdent)
|
||||
{
|
||||
return newKey(_R, machIdent);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(MACH_IDENT); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Extends the basic samskivert user record with special Three Rings business.
|
||||
*/
|
||||
@Entity(name="USER_IDENTS")
|
||||
public class UserIdentRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<UserIdentRecord> _R = UserIdentRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<String> MACH_IDENT = colexp(_R, "machIdent");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 4;
|
||||
|
||||
/** The id of the user in question. */
|
||||
@Id @Column(name="USER_ID")
|
||||
public int userId;
|
||||
|
||||
/** A 'unique' id for a specific machine we have seen the user come from. */
|
||||
@Id @Column(name="MACH_IDENT") @Index(name="ixMachIdent")
|
||||
public String machIdent;
|
||||
|
||||
public UserIdentRecord ()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
public UserIdentRecord (int userId, String machIdent)
|
||||
{
|
||||
super();
|
||||
this.userId = userId;
|
||||
this.machIdent = machIdent;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link UserIdentRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<UserIdentRecord> getKey (int userId, String machIdent)
|
||||
{
|
||||
return newKey(_R, userId, machIdent);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(USER_ID, MACH_IDENT); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.annotation.Index;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
/**
|
||||
* Maps a multiple invitation record to a user.
|
||||
*/
|
||||
@Entity
|
||||
public class UserInvitationRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<UserInvitationRecord> _R = UserInvitationRecord.class;
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<Integer> INVITATION_ID = colexp(_R, "invitationId");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The user id. */
|
||||
@Id
|
||||
public int userId;
|
||||
|
||||
/** The invitation id. */
|
||||
@Index
|
||||
public int invitationId;
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link UserInvitationRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<UserInvitationRecord> getKey (int userId)
|
||||
{
|
||||
return newKey(_R, userId);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(USER_ID); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.depot;
|
||||
|
||||
import java.sql.Date;
|
||||
|
||||
import com.samskivert.depot.Key;
|
||||
import com.samskivert.depot.PersistentRecord;
|
||||
import com.samskivert.depot.annotation.Column;
|
||||
import com.samskivert.depot.annotation.Entity;
|
||||
import com.samskivert.depot.annotation.Id;
|
||||
import com.samskivert.depot.expression.ColumnExp;
|
||||
|
||||
import com.threerings.user.ValidateRecord;
|
||||
|
||||
/**
|
||||
* Emulates {@link ValidateRecord} for the Depot.
|
||||
*/
|
||||
@Entity(name="penders")
|
||||
public class ValidateDepotRecord extends PersistentRecord
|
||||
{
|
||||
// AUTO-GENERATED: FIELDS START
|
||||
public static final Class<ValidateDepotRecord> _R = ValidateDepotRecord.class;
|
||||
public static final ColumnExp<String> SECRET = colexp(_R, "secret");
|
||||
public static final ColumnExp<Integer> USER_ID = colexp(_R, "userId");
|
||||
public static final ColumnExp<Boolean> PERSIST = colexp(_R, "persist");
|
||||
public static final ColumnExp<Date> INSERTED = colexp(_R, "inserted");
|
||||
// AUTO-GENERATED: FIELDS END
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** The secret key. */
|
||||
@Id @Column(length=32)
|
||||
public String secret;
|
||||
|
||||
/** The user id. */
|
||||
public int userId;
|
||||
|
||||
/** The persist? */
|
||||
public boolean persist;
|
||||
|
||||
/** The date inserted. */
|
||||
public Date inserted;
|
||||
|
||||
/**
|
||||
* Creates a ValidateDepotRecord from a ValidateRecord.
|
||||
*/
|
||||
public static ValidateDepotRecord fromValidateRecord (ValidateRecord vr)
|
||||
{
|
||||
ValidateDepotRecord record = new ValidateDepotRecord();
|
||||
record.secret = vr.secret;
|
||||
record.userId = vr.userId;
|
||||
record.persist = vr.persist;
|
||||
record.inserted = vr.inserted;
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ValidateRecord version of this record.
|
||||
*/
|
||||
public ValidateRecord toValidateRecord ()
|
||||
{
|
||||
ValidateRecord record = new ValidateRecord();
|
||||
record.secret = secret;
|
||||
record.userId = userId;
|
||||
record.persist = persist;
|
||||
record.inserted = inserted;
|
||||
return record;
|
||||
}
|
||||
|
||||
// AUTO-GENERATED: METHODS START
|
||||
/**
|
||||
* Create and return a primary {@link Key} to identify a {@link ValidateDepotRecord}
|
||||
* with the supplied key values.
|
||||
*/
|
||||
public static Key<ValidateDepotRecord> getKey (String secret)
|
||||
{
|
||||
return newKey(_R, secret);
|
||||
}
|
||||
|
||||
/** Register the key fields in an order matching the getKey() factory. */
|
||||
static { registerKeyFields(SECRET); }
|
||||
// AUTO-GENERATED: METHODS END
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.tools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.samskivert.util.Config;
|
||||
|
||||
import com.samskivert.jdbc.StaticConnectionProvider;
|
||||
|
||||
import com.threerings.user.AccountAction;
|
||||
import com.threerings.user.depot.AccountActionRepository;
|
||||
|
||||
/**
|
||||
* Provides command line inspection and processing of account actions.
|
||||
*/
|
||||
public class AccountActionTool
|
||||
{
|
||||
public static void main (String[] args)
|
||||
{
|
||||
if (args.length == 0) {
|
||||
System.err.println(USAGE);
|
||||
System.exit(255);
|
||||
}
|
||||
|
||||
Config config = new Config("threerings");
|
||||
try {
|
||||
AccountActionRepository repo = new AccountActionRepository(
|
||||
new StaticConnectionProvider(config.getSubProperties("db")));
|
||||
|
||||
if (args[0].equals("list")) {
|
||||
while (true) {
|
||||
String server = (args.length > 1) ? args[1] : "";
|
||||
List<AccountAction> actions = repo.getActions(server, 999);
|
||||
for (int ii = 0; ii < actions.size(); ii++) {
|
||||
System.out.println(actions.get(ii));
|
||||
}
|
||||
try { Thread.sleep(1000L); } catch (Exception e) {}
|
||||
}
|
||||
|
||||
} else if (args[0].equals("prune")) {
|
||||
repo.pruneActions();
|
||||
|
||||
} else if (args[0].equals("add")) {
|
||||
if (args.length != 3) {
|
||||
System.err.println(USAGE);
|
||||
System.exit(255);
|
||||
}
|
||||
repo.addAction(args[1], Integer.parseInt(args[2]));
|
||||
|
||||
} else if (args[0].equals("process")) {
|
||||
if (args.length != 3) {
|
||||
System.err.println(USAGE);
|
||||
System.exit(255);
|
||||
}
|
||||
repo.noteProcessed(Integer.parseInt(args[1]), args[2]);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
protected static final String USAGE =
|
||||
"Usage: AccountActionTool [prune|list|list server|" +
|
||||
"add account_name action_type|process action_id server]";
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.tools;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import com.samskivert.jdbc.StaticConnectionProvider;
|
||||
import com.samskivert.net.MailUtil;
|
||||
import com.samskivert.servlet.user.User;
|
||||
import com.samskivert.util.ArrayUtil;
|
||||
import com.samskivert.util.Config;
|
||||
|
||||
import com.threerings.user.OOOUser;
|
||||
import com.threerings.user.OOOUserRepository;
|
||||
|
||||
/**
|
||||
* A script for activating (or deactivating) the tester flag on users.
|
||||
*/
|
||||
public class ActivateTesters
|
||||
{
|
||||
public static void main (String[] args)
|
||||
{
|
||||
if (args.length == 0 ||
|
||||
!(args[0].equals("activate") || args[0].equals("clear")) ||
|
||||
(args[0].equals("activate") && args.length < 3)) {
|
||||
System.err.println(USAGE);
|
||||
System.exit(255);
|
||||
}
|
||||
|
||||
Config config = new Config("threerings");
|
||||
try {
|
||||
OOOUserRepository urepo = new OOOUserRepository(
|
||||
new StaticConnectionProvider(config.getSubProperties("db")));
|
||||
|
||||
if (args[0].equals("clear")) {
|
||||
clearTesters(urepo);
|
||||
} else if (args[0].equals("activate")) {
|
||||
activateTesters(urepo, Integer.parseInt(args[1]), args[2]);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void activateTesters (
|
||||
OOOUserRepository urepo, int count, String emailFile)
|
||||
throws Exception
|
||||
{
|
||||
// load up the emails and randomize them
|
||||
BufferedReader bin = new BufferedReader(new FileReader(emailFile));
|
||||
List<String> elist = Lists.newArrayList();
|
||||
String email;
|
||||
int rejected = 0;
|
||||
while ((email = bin.readLine()) != null) {
|
||||
if (!MailUtil.isValidAddress(email)) {
|
||||
// System.err.println("Rejecting invalid address: " + email);
|
||||
rejected++;
|
||||
continue;
|
||||
}
|
||||
elist.add(email);
|
||||
}
|
||||
if (rejected > 0) {
|
||||
System.err.println("Rejected " + rejected +
|
||||
" invalid addresses, accepted " +
|
||||
elist.size() + " valid addresses.");
|
||||
}
|
||||
String[] emails = elist.toArray(new String[elist.size()]);
|
||||
ArrayUtil.shuffle(emails);
|
||||
|
||||
int activated = 0, offset = 0;
|
||||
while (activated < count && offset < emails.length) {
|
||||
// grab the next N addresses from the list and see if any of them
|
||||
// have an account that we can activate as a tester
|
||||
StringBuilder where = new StringBuilder();
|
||||
for (int ii = offset, ll = Math.min(emails.length, offset+CHUNK);
|
||||
ii < ll; ii++) {
|
||||
if (where.length() > 0) {
|
||||
where.append(", ");
|
||||
}
|
||||
where.append("'").append(emails[ii]).append("'");
|
||||
}
|
||||
List<User> users = urepo.lookupUsersWhere("email in (" + where + ")");
|
||||
|
||||
// filter out users that have multiple accounts; cheeky bastards
|
||||
HashSet<String> seen = Sets.newHashSet(), filter = Sets.newHashSet();
|
||||
for (User ruser : users) {
|
||||
OOOUser user = (OOOUser)ruser;
|
||||
if (seen.contains(user.email)) {
|
||||
filter.add(user.email);
|
||||
} else {
|
||||
seen.add(user.email);
|
||||
}
|
||||
}
|
||||
for (Iterator<User> iter = users.iterator(); iter.hasNext(); ) {
|
||||
OOOUser user = (OOOUser)iter.next();
|
||||
if (filter.contains(user.email)) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// now mark anyone that made it through the guantlet as a tester
|
||||
for (Iterator<User> iter = users.iterator(); iter.hasNext(); ) {
|
||||
OOOUser user = (OOOUser)iter.next();
|
||||
if (user.holdsToken(OOOUser.TESTER)) {
|
||||
continue;
|
||||
}
|
||||
user.addToken(OOOUser.TESTER);
|
||||
System.out.println(user.username + " " + user.email);
|
||||
urepo.updateUser(user);
|
||||
activated++;
|
||||
}
|
||||
offset += CHUNK;
|
||||
}
|
||||
|
||||
System.err.println("Activated " + activated + " accounts.");
|
||||
}
|
||||
|
||||
protected static void clearTesters (OOOUserRepository urepo)
|
||||
throws Exception
|
||||
{
|
||||
List<User> users = urepo.lookupUsersWhere("HEX(tokens) like '%04%'");
|
||||
System.err.println("Clearing " + users.size() + " testers.");
|
||||
for (int ii = 0, ll = users.size(); ii < ll; ii++) {
|
||||
OOOUser user = (OOOUser)users.get(ii);
|
||||
user.removeToken(OOOUser.TESTER);
|
||||
urepo.updateUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
protected static final String USAGE =
|
||||
"Usage: ActivateTesters [activate count emails.txt | clear]";
|
||||
|
||||
protected static final int CHUNK = 25;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.threerings.user.tools;
|
||||
|
||||
import com.samskivert.jdbc.StaticConnectionProvider;
|
||||
import com.samskivert.util.Config;
|
||||
|
||||
import com.threerings.user.OOOUserRepository;
|
||||
|
||||
/**
|
||||
* Provides command line inspection and processing of account actions.
|
||||
*/
|
||||
public class UserTool
|
||||
{
|
||||
public static void main (String[] args)
|
||||
{
|
||||
if (args.length == 0) {
|
||||
System.err.println(USAGE);
|
||||
System.exit(255);
|
||||
}
|
||||
|
||||
Config config = new Config("threerings");
|
||||
try {
|
||||
OOOUserRepository repo = new OOOUserRepository(
|
||||
new StaticConnectionProvider(config.getSubProperties("db")));
|
||||
|
||||
if (args[0].equals("prune")) {
|
||||
repo.pruneUsers(PRUNE_DAYS);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
protected static final String USAGE = "Usage: UserTool [prune]";
|
||||
|
||||
/** Users that have been purged from all other games for 30 days are toast. */
|
||||
protected static final int PRUNE_DAYS = 30;
|
||||
}
|
||||
Reference in New Issue
Block a user