package er.modern.directtoweb.delegates; import java.math.BigDecimal; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatterBuilder; import org.joda.time.format.DateTimeParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.directtoweb.D2WContext; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EODatabaseDataSource; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOModelGroup; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EODataSource; import com.webobjects.eocontrol.EOKeyValueQualifier; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSTimestamp; import com.webobjects.foundation._NSUtilities; import er.extensions.eof.ERXQ; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXStringUtilities; import er.extensions.foundation.ERXValueUtilities; import er.extensions.localization.ERXLocalizer; /** * Delegate handling search values that are to be applied to multiple * attributes, e.g. id, email and date. Components need to implement the * {@link ERMD2WQueryComponent} interface. One or more attributes to qualify on * may be defined via the searchKey D2W key. If searchKey is null, * keyWhenRelationship will be evaluated. * * If you wish to search for dates, you likely want to specify custom patterns * via * er.modern.directtoweb.delegates.ERMD2WAttributeQueryDelegate.datePatterns. * The default patterns are for use with ERXTimestampFormatter's default of * month, day, year, separated by '/'.<br> * If "ddMM" or "MMdd" are defined, a four digit query string will be * interpreted as a day/month combination. If these patterns are not defined, * but "yyyy" is, then a four digit query string will be interpreted as a year. * * For usage examples, see @ERMD2WListFilter and @ERMD2WEditToOneTypeAhead. * * @property * er.modern.directtoweb.delegates.ERMD2WAttributeQueryDelegate.datePatterns * * @author fpeters */ public class ERMD2WAttributeQueryDelegate { private static final Logger log = LoggerFactory.getLogger(ERMD2WAttributeQueryDelegate.class); public static final ERMD2WAttributeQueryDelegate instance = new ERMD2WAttributeQueryDelegate(); /** * Simple interface required for use of {@link ERMD2WAttributeQueryDelegate} * */ public interface ERMD2WQueryComponent { public String searchValue(); public D2WContext d2wContext(); public EODataSource dataSource(); } public EOQualifier buildQualifier(ERMD2WQueryComponent sender) { EOQualifier qualifier = null; Integer typeAheadMinimumCharacterCount = ERXValueUtilities .IntegerValueWithDefault( sender.d2wContext().valueForKey("typeAheadMinimumCharacterCount"), 3); if (sender.searchValue() != null && sender.searchValue().length() >= typeAheadMinimumCharacterCount) { NSMutableArray<EOQualifier> qualifiers = new NSMutableArray<EOQualifier>(); for (String anAttributeName : searchKey(sender)) { EOEntity entity = null; if (sender.dataSource() != null) { entity = EOUtilities.entityNamed(sender.dataSource().editingContext(), sender.dataSource().classDescriptionForObjects() .entityName()); } else { // sender should be a to-one relationship component entity = EOModelGroup.defaultGroup().entityNamed( (String) sender.d2wContext().valueForKey( "destinationEntityName")); } EOAttribute attribute = entity.attributeNamed(anAttributeName); if (attribute == null) { attribute = resolveDestinationAttribute(anAttributeName, entity, attribute); } if (attribute != null) { String attributeClassName = attribute.className(); if ("java.lang.Number".equals(attributeClassName)) { buildNumberQualifier(sender, qualifiers, anAttributeName); } else if ("java.math.BigDecimal".equals(attributeClassName)) { buildBigDecimalQualifier(sender, qualifiers, anAttributeName); } else if (attributeClassName.toLowerCase().contains("enum")) { buildEnumQualifier(sender, qualifiers, anAttributeName, attributeClassName); } else if ("com.webobjects.foundation.NSTimestamp" .equals(attributeClassName)) { buildDateQualifier(sender, qualifiers, anAttributeName); } else { qualifiers.addObject(new EOKeyValueQualifier(anAttributeName, selector, "*" + sender.searchValue() + "*")); } } } qualifier = ERXQ.or(qualifiers); } // handle an existing extra qualifier EOQualifier extraRestrictingQ = (EOQualifier) sender.d2wContext().valueForKey( "extraRestrictingQualifier"); if (extraRestrictingQ != null) { qualifier = ERXQ.and(qualifier, extraRestrictingQ); } return qualifier; } private static final NSSelector<?> selector = EOQualifier.QualifierOperatorCaseInsensitiveLike; @SuppressWarnings("unchecked") private NSArray<String> searchKey(ERMD2WQueryComponent sender) { NSArray<String> searchKey = null; if (sender.d2wContext().valueForKey("searchKey") == null && sender.dataSource() != null && sender.dataSource() instanceof EODatabaseDataSource) { // fallback, choose first attribute searchKey = new NSArray<String>( (String) ((EODatabaseDataSource) sender.dataSource()).entity() .classPropertyNames().objectAtIndex(0)); } else if (sender.d2wContext().valueForKey("searchKey") == null && sender.d2wContext().valueForKey("keyWhenRelationship") != null && sender.d2wContext().valueForKey("keyWhenRelationship") instanceof String) { searchKey = new NSArray<String>((String) sender.d2wContext().valueForKey( "keyWhenRelationship")); } else if (sender.d2wContext().valueForKey("searchKey") instanceof String) { searchKey = new NSArray<String>((String) sender.d2wContext().valueForKey( "searchKey")); } else { searchKey = (NSArray<String>) sender.d2wContext().valueForKey("searchKey"); } return searchKey; } private void buildDateQualifier(ERMD2WQueryComponent sender, NSMutableArray<EOQualifier> qualifiers, String anAttributeName) { // don't attempt to parse when the string's too short or too long if (sender.searchValue().length() > 3 && sender.searchValue().length() < 11) { try { // same default as ERXTimestampformatter: month, day, year // separated by '/' NSArray<String> defaultPatterns = new NSArray<String>("MM/dd", "MM/dd/", "MM/dd/yy", "MM/DD/yyyy", "yyyy"); @SuppressWarnings("unchecked") NSArray<String> patterns = ERXProperties .arrayForKeyWithDefault( "er.modern.directtoweb.delegates.ERMD2WAttributeQueryDelegate.datePatterns", defaultPatterns); // prepare a parser for the given patterns DateTimeParser[] parsers = new DateTimeParser[patterns.count()]; for (int i = 0; i < patterns.count(); i++) { parsers[i] = DateTimeFormat.forPattern(patterns.objectAtIndex(i)) .getParser(); } DateTimeFormatter parsingFormatter = new DateTimeFormatterBuilder() .append(null, parsers).toFormatter(); // attempt parse the search value as a date DateTime date = parsingFormatter.parseDateTime(sender.searchValue()); // if a parser w/o a year was applied, the date will default to // 1970 or 2000, which we modify to the current year if (date != null && date.getYear() == 1970 && !sender.searchValue().contains("70")) { date = date.plusYears(new DateTime().getYear() - 1970); } else if (date != null && date.getYear() == 2000 && !sender.searchValue().contains("00")) { date = date.plusYears(new DateTime().getYear() - 2000); } // should the query string be interpreted as a year? if (!(patterns.contains("ddMM") || patterns.contains("MMdd")) && patterns.contains("yyyy") && sender.searchValue().matches("^\\d{4}$")) { // search for a whole year EOQualifier dateQ = ERXQ.between( anAttributeName, new NSTimestamp(date.minusHours(date.getHourOfDay()) .getMillis()), new NSTimestamp(date.plusYears(1) .minusHours(date.getHourOfDay()).getMillis())); qualifiers.addObject(dateQ); } // no, search for whole days, i.e. 0-24 hours else if (date != null && date.getYear() > 1950) { EOQualifier dateQ = ERXQ.between( anAttributeName, new NSTimestamp(date.minusHours(date.getHourOfDay()) .getMillis()), new NSTimestamp(date.plusDays(1) .minusHours(date.getHourOfDay()).getMillis())); qualifiers.addObject(dateQ); } } catch (IllegalArgumentException iae) { log.debug("Failed to prepare date qualifier:", iae); } } } private void buildEnumQualifier(ERMD2WQueryComponent sender, NSMutableArray<EOQualifier> qualifiers, String anAttributeName, String attributeClassName) { @SuppressWarnings("unchecked") Class<? extends Enum<?>> klass = _NSUtilities.classWithName(attributeClassName); NSMutableArray<Enum<?>> matchingEnums = new NSMutableArray<Enum<?>>(); if (klass != null && klass.isEnum()) { Enum<?>[] em = klass.getEnumConstants(); for (int i = 0, length = em.length; i < length; ++i) { Enum<?> anEnum = em[i]; // localizing, to make sure the value is in the cache ERXLocalizer.currentLocalizer().localizedStringForKey(anEnum.name()); String unlocalizedName = (String) ERXLocalizer.currentLocalizer().cache() .objectForKey(anEnum.name()); if (unlocalizedName.toLowerCase().contains( sender.searchValue().toLowerCase())) { matchingEnums.addObject(anEnum); } } } for (Enum<?> anEnum : matchingEnums) { qualifiers .addObject(new EOKeyValueQualifier(anAttributeName, ERXQ.EQ, anEnum)); } } private void buildBigDecimalQualifier(ERMD2WQueryComponent sender, NSMutableArray<EOQualifier> qualifiers, String anAttributeName) { Number numericValue = null; try { numericValue = new BigDecimal(sender.searchValue()); qualifiers.addObject(new EOKeyValueQualifier(anAttributeName, EOQualifier.QualifierOperatorEqual, numericValue)); } catch (NumberFormatException nfe) { } } private void buildNumberQualifier(ERMD2WQueryComponent sender, NSMutableArray<EOQualifier> qualifiers, String anAttributeName) { Number numericValue = null; try { numericValue = Integer.valueOf(sender.searchValue()); qualifiers.addObject(new EOKeyValueQualifier(anAttributeName, EOQualifier.QualifierOperatorEqual, numericValue)); } catch (NumberFormatException nfe) { } } private EOAttribute resolveDestinationAttribute(String anAttributeName, EOEntity entity, EOAttribute attribute) { /* * we have a relationship here, let's find the destination attribute */ EORelationship relationship = entity.anyRelationshipNamed(ERXStringUtilities .keyPathWithoutLastProperty(anAttributeName)); if (relationship == null) { relationship = entity.anyRelationshipNamed(ERXStringUtilities .firstPropertyKeyInKeyPath(anAttributeName)); if (relationship != null) { // this is a multi-hop relationship, recurse to resolve it attribute = resolveDestinationAttribute( ERXStringUtilities.keyPathWithoutFirstProperty(anAttributeName), relationship.destinationEntity(), attribute); } else { log.warn("Failed to resolve destination attribute for key path: {}", anAttributeName); } } else { entity = relationship.destinationEntity(); if (entity != null) { String resolvedAttributeName = ERXStringUtilities .lastPropertyKeyInKeyPath(anAttributeName); attribute = entity.attributeNamed(resolvedAttributeName); // TODO set distinct as this may be a to-many } } return attribute; } }