package org.karmaexchange.resources.derived;
import static java.lang.String.format;
import static org.karmaexchange.util.OfyService.ofy;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.xml.bind.annotation.XmlRootElement;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Delegate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.karmaexchange.auth.GlobalUidMapping;
import org.karmaexchange.dao.Address;
import org.karmaexchange.dao.AssociatedOrganization;
import org.karmaexchange.dao.BaseDao;
import org.karmaexchange.dao.Event;
import org.karmaexchange.dao.AssociatedOrganization.Association;
import org.karmaexchange.dao.Event.EventParticipant;
import org.karmaexchange.dao.GeoPtWrapper;
import org.karmaexchange.dao.Event.ParticipantType;
import org.karmaexchange.dao.Location;
import org.karmaexchange.dao.User;
import org.karmaexchange.dao.derived.EventSourceInfo;
import org.karmaexchange.dao.derived.SourceEventNamespaceDao;
import org.karmaexchange.dao.IdBaseDao;
import org.karmaexchange.dao.KeyWrapper;
import org.karmaexchange.dao.Organization;
import org.karmaexchange.resources.msg.ErrorResponseMsg;
import org.karmaexchange.resources.msg.ErrorResponseMsg.ErrorInfo;
import org.karmaexchange.util.GeocodingService;
import org.karmaexchange.util.SalesforceUtil;
import com.google.appengine.api.datastore.GeoPt;
import com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
@XmlRootElement
@Data
public class SourceEvent {
private static long EVENT_ID = 1; // There is a 1-1 mapping between SourceEvent and Event.
private static final Logger log = Logger.getLogger(SourceEvent.class.getName());
private List<SourceEventParticipant> sourceParticipants = Lists.newArrayList();
private SourceAssociatedOrganization sourceAssociatedOrg;
/*
* We delegate instead of extending event because Objectify seems to require that all subclasses
* be entities themselves; which we don't want.
*/
@Delegate(types={Event.class, IdBaseDao.class, BaseDao.class})
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private Event event = new Event();
public Event toEvent(EventSourceInfo sourceInfo) {
validate(sourceInfo.getOrgKey());
if (event.getDescriptionHtml() != null) {
event.setDescriptionHtml(
SalesforceUtil.processRichTextField(event.getDescriptionHtml(), sourceInfo));
}
if (event.getLocationInformationHtml() != null) {
event.setLocationInformationHtml(
SalesforceUtil.processRichTextField(event.getLocationInformationHtml(), sourceInfo));
}
if (event.getExternalRegistrationDetailsHtml() != null) {
event.setExternalRegistrationDetailsHtml(
SalesforceUtil.processRichTextField(
event.getExternalRegistrationDetailsHtml(), sourceInfo));
}
if (event.getLocation() != null) {
Location loc = event.getLocation();
if (loc.getTitle() != null) {
loc.setTitle(loc.getTitle().trim());
}
Address addr = loc.getAddress();
if (addr != null) {
if (addr.getGeoPt() == null) {
String geocodeableAddr = addr.toGeocodeableString();
// Do a synchronous API call to the Google geocoding service.
GeoPt geoPt = GeocodingService.getGeoPt(geocodeableAddr);
if (geoPt != null) {
addr.setGeoPt(GeoPtWrapper.create(geoPt));
}
} else {
// The geoPt was explicitly specified. Mark it as such.
addr.getGeoPt().setExplicit(true);
}
}
}
event.setOwner(
SourceEventNamespaceDao.createKey(
sourceInfo.getOrgKey(),
event.getSourceEventInfo().getId()).getString());
event.setId(EVENT_ID);
event.setOrganization(KeyWrapper.create(sourceInfo.getOrgKey()));
mapAssociatedOrg(sourceInfo);
mapSourceParticipants(sourceInfo.getOrgKey());
return event;
}
private void mapAssociatedOrg(EventSourceInfo sourceInfo) {
if (sourceAssociatedOrg != null) {
AssociatedOrganization assocOrg =
sourceAssociatedOrg.toAssociatedOrganization(sourceInfo);
event.getAssociatedOrganizations().add(assocOrg);
}
}
private void mapSourceParticipants(Key<Organization> eventListingOrgKey) {
List<Key<GlobalUidMapping>> sourceParticipantMappingsKeys =
Lists.newArrayList();
for (SourceEventParticipant sourceParticipant : sourceParticipants) {
sourceParticipantMappingsKeys.add(
sourceParticipant.user.createGlobalUidMappingKey());
}
Map<Key<GlobalUidMapping>, GlobalUidMapping> sourceParticipantMappings =
ofy().load().keys(sourceParticipantMappingsKeys);
List<EventParticipant> participants =
Lists.newArrayList();
for (SourceEventParticipant sourceParticipant : sourceParticipants) {
GlobalUidMapping mapping =
sourceParticipantMappings.get(sourceParticipant.user.createGlobalUidMappingKey());
Key<User> userKey;
if (mapping == null) {
userKey = User.upsertNewUser(
sourceParticipant.user.createUser(eventListingOrgKey));
} else {
userKey = mapping.getUserKey();
}
participants.add(sourceParticipant.toEventParticipant(userKey));
}
event.setParticipants(participants);
}
public static Key<Event> createKey(EventSourceInfo sourceInfo, String sourceKey) {
return Key.<Event>create(
SourceEventNamespaceDao.createKey(sourceInfo.getOrgKey(), sourceKey),
Event.class,
EVENT_ID);
}
private void validate(Key<Organization> orgKey) {
if (event.getSourceEventInfo() == null) {
throw ErrorResponseMsg.createException("sourceEventInfo must be specified",
ErrorInfo.Type.BAD_REQUEST);
}
if (event.getSourceEventInfo().getId() == null) {
throw ErrorResponseMsg.createException("sourceEventInfo.id must be specified",
ErrorInfo.Type.BAD_REQUEST);
}
if (event.getSourceEventInfo().getLastModifiedDate() == null) {
throw ErrorResponseMsg.createException("sourceEventInfo.lastModifiedDate must be specified",
ErrorInfo.Type.BAD_REQUEST);
}
if (event.getKey() != null) {
throw ErrorResponseMsg.createException(
"key is a derived field and can not be specified",
ErrorInfo.Type.BAD_REQUEST);
}
if (event.getOrganization() != null) {
throw ErrorResponseMsg.createException(
"organization field should not be specified",
ErrorInfo.Type.BAD_REQUEST);
}
// This restriction is to simplify the code. In the future we can support a mix of
// descriptions.
if (event.getDescription() != null) {
throw ErrorResponseMsg.createException(
"only an html description can be specified for derived events",
ErrorInfo.Type.BAD_REQUEST);
}
if (!event.getParticipants().isEmpty()) {
throw ErrorResponseMsg.createException(
"only sourceParticipants can be specified for derived events",
ErrorInfo.Type.BAD_REQUEST);
}
Iterator<SourceEventParticipant> sourceParticipantsIter = sourceParticipants.iterator();
while (sourceParticipantsIter.hasNext()) {
SourceEventParticipant sourceParticipant =
sourceParticipantsIter.next();
BaseSourceUser sourceUser = sourceParticipant.user;
if (sourceUser == null) {
throw ErrorResponseMsg.createException(
"required field 'user' is null for sourceParticipant",
ErrorInfo.Type.BAD_REQUEST);
}
if (sourceUser.getEmail() == null) {
sourceParticipantsIter.remove();
log.warning(
format("required field 'email' missing: " +
"skipping user(%s,%s) for source event(%s, org=%s)",
sourceUser.getFirstName(), sourceUser.getLastName(), event.getSourceEventInfo().getId(),
orgKey.toString()));
}
}
if (sourceAssociatedOrg != null) {
sourceAssociatedOrg.validate();
}
}
@Data
@NoArgsConstructor
public static final class SourceEventParticipant {
private BaseSourceUser user;
private ParticipantType type;
public int numVolunteers;
public double hoursWorked;
public EventParticipant toEventParticipant(Key<User> userKey) {
return new EventParticipant(userKey, type, numVolunteers, hoursWorked);
}
}
@Data
@NoArgsConstructor
public static final class SourceAssociatedOrganization {
private SourceOrganization org;
private Association association;
public void validate() {
if (org == null) {
throw ErrorResponseMsg.createException("associated org field 'org' must be specified",
ErrorInfo.Type.BAD_REQUEST);
}
org.validate();
}
public AssociatedOrganization toAssociatedOrganization(EventSourceInfo sourceInfo) {
Key<Organization> assocOrgKey =
org.lookupOrCreateOrg(sourceInfo);
return new AssociatedOrganization(assocOrgKey, org.getName(), association);
}
}
}