RDAP: Implement entity name search

Adds the ability to search for entities (contacts and registrars) by name.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=130305930
This commit is contained in:
mountford 2016-08-15 11:48:40 -07:00 committed by Ben McIlwain
parent 64abebec82
commit 160266f37a
8 changed files with 368 additions and 66 deletions

View file

@ -80,4 +80,8 @@
<property name="deletionTime" direction="asc"/>
<property name="fullyQualifiedHostName" direction="asc"/>
</datastore-index>
<datastore-index kind="ContactResource" ancestor="false" source="manual">
<property name="deletionTime" direction="asc"/>
<property name="searchName" direction="asc"/>
</datastore-index>
</datastore-indexes>

View file

@ -24,6 +24,7 @@ import com.google.common.collect.Lists;
import com.googlecode.objectify.annotation.Cache;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
@ -85,6 +86,15 @@ public class ContactResource extends EppResource implements ForeignKeyedEppResou
@XmlTransient
PostalInfo internationalizedPostalInfo;
/**
* Contact name used for name searches. This is set automatically to be the internationalized
* postal name, or if null, the localized postal name, or if that is null as well, null. Personal
* info; cleared by wipeOut().
*/
@Index
@XmlTransient
String searchName;
/** Contacts voice number. Personal info; cleared by wipeOut(). */
@IgnoreSave(IfNull.class)
ContactPhoneNumber voice;
@ -250,6 +260,16 @@ public class ContactResource extends EppResource implements ForeignKeyedEppResou
@Override
public ContactResource build() {
// Set the searchName using the internationalized and localized postal info names.
if ((getInstance().internationalizedPostalInfo != null)
&& (getInstance().internationalizedPostalInfo.getName() != null)) {
getInstance().searchName = getInstance().internationalizedPostalInfo.getName();
} else if ((getInstance().localizedPostalInfo != null)
&& (getInstance().localizedPostalInfo.getName() != null)) {
getInstance().searchName = getInstance().localizedPostalInfo.getName();
} else {
getInstance().searchName = null;
}
return super.build();
}
}

View file

@ -786,7 +786,7 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
}
}
/** Load a registrar entity by its client id. */
/** Load a registrar entity by its client id outside of a transaction. */
@Nullable
public static Registrar loadByClientId(final String clientId) {
return ofy().doTransactionless(new Work<Registrar>() {
@ -801,7 +801,7 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
}
/**
* Load registrar entities by client id range.
* Load registrar entities by client id range outside of a transaction.
*
* @param clientIdStart returned registrars will have a client id greater than or equal to this
* @param clientIdAfterEnd returned registrars will have a client id less than this
@ -820,6 +820,41 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
}});
}
/** Load a registrar entity by its name outside of a transaction. */
@Nullable
public static Registrar loadByName(final String name) {
return ofy().doTransactionless(new Work<Registrar>() {
@Override
public Registrar run() {
return ofy().load()
.type(Registrar.class)
.filter("registrarName", name)
.first()
.now();
}});
}
/**
* Load registrar entities by registrar name range, inclusive of the start but not the end,
* outside of a transaction.
*
* @param nameStart returned registrars will have a name greater than or equal to this
* @param nameAfterEnd returned registrars will have a name less than this
* @param resultSetMaxSize the maximum number of registrar entities to be returned
*/
public static Iterable<Registrar> loadByNameRange(
final String nameStart, final String nameAfterEnd, final int resultSetMaxSize) {
return ofy().doTransactionless(new Work<Iterable<Registrar>>() {
@Override
public Iterable<Registrar> run() {
return ofy().load()
.type(Registrar.class)
.filter("registrarName >=", nameStart)
.filter("registrarName <", nameAfterEnd)
.limit(resultSetMaxSize);
}});
}
/** Loads all registrar entities. */
public static Iterable<Registrar> loadAll() {
return ofy().load().type(Registrar.class).ancestor(getCrossTldKey());

View file

@ -24,7 +24,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Booleans;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.cmd.Query;
import google.registry.config.ConfigModule.Config;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact;
@ -34,7 +33,6 @@ import google.registry.request.Action;
import google.registry.request.HttpException;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.HttpException.NotFoundException;
import google.registry.request.HttpException.NotImplementedException;
import google.registry.request.HttpException.UnprocessableEntityException;
import google.registry.request.Parameter;
import google.registry.util.Clock;
@ -88,15 +86,8 @@ public class RdapEntitySearchAction extends RdapActionBase {
ImmutableList<ImmutableMap<String, Object>> results;
if (fnParam.isPresent()) {
// syntax: /rdap/entities?fn=Bobby%20Joe*
// TODO(b/25973399): implement entity name search, and move the comment below to that routine
// As per Gustavo Lozano of ICANN, registrar name search should be by registrar name only, not
// by registrar contact name:
//
// The search is by registrar name only. The profile is supporting the functionality defined
// in the Base Registry Agreement (see 1.6 of Section 4 of the Base Registry Agreement,
// https://newgtlds.icann.org/sites/default/files/agreements/
// agreement-approved-09jan14-en.htm).
throw new NotImplementedException("Entity name search not implemented");
// The name is the contact name or registrar name (not registrar contact name).
results = searchByName(RdapSearchPattern.create(fnParam.get(), false), now);
} else {
// syntax: /rdap/entities?handle=12345-*
// The handle is either the contact roid or the registrar clientId.
@ -111,6 +102,57 @@ public class RdapEntitySearchAction extends RdapActionBase {
return builder.build();
}
/**
* Searches for entities by name, returning a JSON array of entity info maps.
*
* <p>As per Gustavo Lozano of ICANN, registrar name search should be by registrar name only, not
* by registrar contact name:
*
* <p>The search is by registrar name only. The profile is supporting the functionality defined
* in the Base Registry Agreement (see 1.6 of Section 4 of the Base Registry Agreement,
* https://newgtlds.icann.org/sites/default/files/agreements/
* agreement-approved-09jan14-en.htm).
*
* <p>According to RFC 7482 section 6.1, punycode is only used for domain name labels, so we can
* assume that entity names are regular unicode.
*/
private ImmutableList<ImmutableMap<String, Object>>
searchByName(final RdapSearchPattern partialStringQuery, DateTime now) throws HttpException {
// Handle queries without a wildcard -- load by name, which may not be unique.
if (!partialStringQuery.getHasWildcard()) {
Registrar registrar = Registrar.loadByName(partialStringQuery.getInitialString());
return makeSearchResults(
ofy().load()
.type(ContactResource.class)
.filter("searchName", partialStringQuery.getInitialString())
.filter("deletionTime", END_OF_TIME)
.limit(rdapResultSetMaxSize),
(registrar == null)
? ImmutableList.<Registrar>of() : ImmutableList.of(registrar),
now);
// Handle queries with a wildcard, but no suffix. For contact resources, the deletion time will
// always be END_OF_TIME for non-deleted records; unlike domain resources, we don't need to
// worry about deletion times in the future. That allows us to use an equality query for the
// deletion time.
} else if (partialStringQuery.getSuffix() == null) {
return makeSearchResults(
ofy().load()
.type(ContactResource.class)
.filter("searchName >=", partialStringQuery.getInitialString())
.filter("searchName <", partialStringQuery.getNextInitialString())
.filter("deletionTime", END_OF_TIME)
.limit(rdapResultSetMaxSize),
Registrar.loadByNameRange(
partialStringQuery.getInitialString(),
partialStringQuery.getNextInitialString(),
rdapResultSetMaxSize),
now);
// Don't allow suffixes in entity name search queries.
} else {
throw new UnprocessableEntityException("Suffixes not allowed in entity name searches");
}
}
/** Searches for entities by handle, returning a JSON array of entity info maps. */
private ImmutableList<ImmutableMap<String, Object>> searchByHandle(
final RdapSearchPattern partialStringQuery, DateTime now) throws HttpException {
@ -121,63 +163,62 @@ public class RdapEntitySearchAction extends RdapActionBase {
.id(partialStringQuery.getInitialString())
.now();
Registrar registrar = Registrar.loadByClientId(partialStringQuery.getInitialString());
ImmutableList.Builder<ImmutableMap<String, Object>> builder = new ImmutableList.Builder<>();
if ((contactResource != null) && contactResource.getDeletionTime().isEqual(END_OF_TIME)) {
// As per Andy Newton on the regext mailing list, contacts by themselves have no role, since
// they are global, and might have different roles for different domains.
builder.add(RdapJsonFormatter.makeRdapJsonForContact(
contactResource,
false,
Optional.<DesignatedContact.Type>absent(),
rdapLinkBase,
rdapWhoisServer,
now));
}
if ((registrar != null) && registrar.isActiveAndPubliclyVisible()) {
builder.add(RdapJsonFormatter.makeRdapJsonForRegistrar(
registrar, false, rdapLinkBase, rdapWhoisServer, now));
}
return builder.build();
return makeSearchResults(
((contactResource == null) || !contactResource.getDeletionTime().isEqual(END_OF_TIME))
? ImmutableList.<ContactResource>of() : ImmutableList.of(contactResource),
(registrar == null)
? ImmutableList.<Registrar>of() : ImmutableList.of(registrar),
now);
// Handle queries with a wildcard, but no suffix. For contact resources, the deletion time will
// always be END_OF_TIME for non-deleted records; unlike domain resources, we don't need to
// worry about deletion times in the future. That allows us to use an equality query for the
// deletion time.
} else if (partialStringQuery.getSuffix() == null) {
ImmutableList.Builder<ImmutableMap<String, Object>> builder = new ImmutableList.Builder<>();
Query<ContactResource> query = ofy().load()
.type(ContactResource.class)
.filterKey(">=", Key.create(ContactResource.class, partialStringQuery.getInitialString()))
.filterKey(
"<", Key.create(ContactResource.class, partialStringQuery.getNextInitialString()))
.filter("deletionTime", END_OF_TIME)
.limit(rdapResultSetMaxSize);
for (ContactResource contactResource : query) {
builder.add(RdapJsonFormatter.makeRdapJsonForContact(
contactResource,
false,
Optional.<DesignatedContact.Type>absent(),
rdapLinkBase,
rdapWhoisServer,
now));
}
for (Registrar registrar
: Registrar.loadByClientIdRange(
return makeSearchResults(
ofy().load()
.type(ContactResource.class)
.filterKey(
">=", Key.create(ContactResource.class, partialStringQuery.getInitialString()))
.filterKey(
"<", Key.create(ContactResource.class, partialStringQuery.getNextInitialString()))
.filter("deletionTime", END_OF_TIME)
.limit(rdapResultSetMaxSize),
Registrar.loadByClientIdRange(
partialStringQuery.getInitialString(),
partialStringQuery.getNextInitialString(),
rdapResultSetMaxSize)) {
if (registrar.isActiveAndPubliclyVisible()) {
builder.add(RdapJsonFormatter.makeRdapJsonForRegistrar(
registrar, false, rdapLinkBase, rdapWhoisServer, now));
}
}
// In theory, there could be more results than our max size, so limit the size.
ImmutableList<ImmutableMap<String, Object>> resultSet = builder.build();
return (resultSet.size() <= rdapResultSetMaxSize)
? resultSet
: resultSet.subList(0, rdapResultSetMaxSize);
rdapResultSetMaxSize),
now);
// Don't allow suffixes in entity handle search queries.
} else {
throw new UnprocessableEntityException("Suffixes not allowed in entity handle searches");
}
}
/** Builds a JSON array of entity info maps based on the specified contacts and registrars. */
private ImmutableList<ImmutableMap<String, Object>> makeSearchResults(
Iterable<ContactResource> contactResources, Iterable<Registrar> registrars, DateTime now)
throws HttpException {
ImmutableList.Builder<ImmutableMap<String, Object>> builder = new ImmutableList.Builder<>();
for (ContactResource contact : contactResources) {
// As per Andy Newton on the regext mailing list, contacts by themselves have no role, since
// they are global, and might have different roles for different domains.
builder.add(RdapJsonFormatter.makeRdapJsonForContact(
contact,
false,
Optional.<DesignatedContact.Type>absent(),
rdapLinkBase,
rdapWhoisServer, now));
}
for (Registrar registrar : registrars) {
if (registrar.isActiveAndPubliclyVisible()) {
builder.add(RdapJsonFormatter.makeRdapJsonForRegistrar(
registrar, false, rdapLinkBase, rdapWhoisServer, now));
}
}
// In theory, there could be more results than our max size, so limit the size.
ImmutableList<ImmutableMap<String, Object>> resultSet = builder.build();
return (resultSet.size() <= rdapResultSetMaxSize)
? resultSet
: resultSet.subList(0, rdapResultSetMaxSize);
}
}