<NME> AttributesGeocacheFilter.java
<BEF> package cgeo.geocaching.filters.core;

import cgeo.geocaching.enumerations.CacheAttribute;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.storage.SqlBuilder;

import java.util.HashMap;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class AttributesGeocacheFilter extends BaseGeocacheFilter {


    private final Set<String> attributes = new HashSet<>();
    private boolean inverse = false;

    public void setAttributes(final Map<CacheAttribute, Boolean> atts) {
        this.attributes.clear();
        for (Map.Entry<CacheAttribute, Boolean> entry : atts.entrySet()) {
            if (entry.getValue() != null) {
                this.attributes.add(entry.getKey().getValue(entry.getValue()));
            }
        }
    }
                if (entry.getValue()) {
                    attributesRaw.add(entry.getKey().rawName);
                }
            }
        }
    }

    public void setInverse(final boolean inverse) {

    public Map<CacheAttribute, Boolean> getAttributes() {
        final Map<CacheAttribute, Boolean> result = new HashMap<>();
        for (String attString : attributes) {
            final CacheAttribute ca = CacheAttribute.getByRawName(CacheAttribute.trimAttributeName(attString));
            if (ca != null) {
                result.put(ca, CacheAttribute.isEnabled(attString));
    public void setSources(final int sources) {
        this.sources = sources;
    }

    public int getSources() {
        return sources;
    }
        if (cache == null) {
            return null;
        }
        //check if cache attributes are not filled -> means that this filter is inconclusive
        if (!attributes.isEmpty() && cache.getAttributes().isEmpty() && !cache.isDetailed()) {
            return null;
        }

        int found = 0;
        for (String cacheAtt : cache.getAttributes()) {
            if (attributes.contains(cacheAtt)) {
                found++;
            }
        }
            return true;
        }

        //check if cache attributes are not filled -> means that this filter is inconclusive
        if (cache.getAttributes().isEmpty() && !cache.isDetailed()) {
            return null;
        }

        int found = 0;
                sqlBuilder.openWhere(SqlBuilder.WhereType.NOT);
            }
            sqlBuilder.openWhere(SqlBuilder.WhereType.AND);
            for (String att : attributes) {
                final String attTableId = sqlBuilder.getNewTableId();
                sqlBuilder.addWhere("EXISTS (SELECT geocode FROM cg_attributes " + attTableId + " WHERE " + attTableId + ".geocode = " + sqlBuilder.getMainTableId() + ".geocode AND attribute = ?)", att);
            }
            sqlBuilder.closeWhere();
            if (inverse) {
    public void addToSql(final SqlBuilder sqlBuilder) {
        if (attributes.isEmpty()) {
            sqlBuilder.addWhereTrue();
        } else {
            if (inverse) {
                sqlBuilder.openWhere(SqlBuilder.WhereType.NOT);
    @Override
    public void setConfig(final String[] value) {
        attributes.clear();
        if (value.length == 0) {
            inverse = false;
        } else {
            inverse = BooleanUtils.toBoolean(value[0]);
            for (int idx = 1; idx < value.length; idx++) {
                attributes.add(value[idx]);
            }
        }
    }
    public void setConfig(final ExpressionConfig config) {
        this.inverse = config.getFirstValue(CONFIG_KEY_INVERSE, false, BooleanUtils::toBoolean);
        this.sources = config.getFirstValue(CONFIG_KEY_SOURCES, Settings.getAttributeFilterSources(), Integer::parseInt);
        final String[] result = new String[attributes.size() + 1];
        result[0] = Boolean.toString(inverse);
        int idx = 1;
        for (String att : attributes) {
            result[idx++] = att;
        }
        return result;
    }
}
    }

    @Override
    protected String getUserDisplayableConfig() {
        if (attributes.isEmpty()) {
            return LocalizationUtils.getString(R.string.cache_filter_userdisplay_none);
        }
        if (attributes.size() > 1) {
            return LocalizationUtils.getPlural(R.plurals.cache_filter_userdisplay_multi_item, attributes.size());
        }

        return attributes.keySet().iterator().next();
    }
}

<MSG> fix #8620: filtering for attributes for oc caches (#10646)


<DFF> @@ -1,8 +1,10 @@
 package cgeo.geocaching.filters.core;
 
+import cgeo.geocaching.R;
 import cgeo.geocaching.enumerations.CacheAttribute;
 import cgeo.geocaching.models.Geocache;
 import cgeo.geocaching.storage.SqlBuilder;
+import cgeo.geocaching.utils.LocalizationUtils;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -14,14 +16,19 @@ import org.apache.commons.lang3.BooleanUtils;
 public class AttributesGeocacheFilter extends BaseGeocacheFilter {
 
 
-    private final Set<String> attributes = new HashSet<>();
+    private final Map<String, String> attributes = new HashMap<>();
+    private final Set<String> attributesRaw = new HashSet<>();
     private boolean inverse = false;
 
     public void setAttributes(final Map<CacheAttribute, Boolean> atts) {
         this.attributes.clear();
+        this.attributesRaw.clear();
         for (Map.Entry<CacheAttribute, Boolean> entry : atts.entrySet()) {
             if (entry.getValue() != null) {
-                this.attributes.add(entry.getKey().getValue(entry.getValue()));
+                this.attributes.put(entry.getKey().getValue(entry.getValue()), entry.getValue() ? entry.getKey().rawName : null);
+                if (entry.getValue()) {
+                    attributesRaw.add(entry.getKey().rawName);
+                }
             }
         }
     }
@@ -36,7 +43,7 @@ public class AttributesGeocacheFilter extends BaseGeocacheFilter {
 
     public Map<CacheAttribute, Boolean> getAttributes() {
         final Map<CacheAttribute, Boolean> result = new HashMap<>();
-        for (String attString : attributes) {
+        for (String attString : attributes.keySet()) {
             final CacheAttribute ca = CacheAttribute.getByRawName(CacheAttribute.trimAttributeName(attString));
             if (ca != null) {
                 result.put(ca, CacheAttribute.isEnabled(attString));
@@ -50,14 +57,18 @@ public class AttributesGeocacheFilter extends BaseGeocacheFilter {
         if (cache == null) {
             return null;
         }
+        if (attributes.isEmpty()) {
+            return true;
+        }
+
         //check if cache attributes are not filled -> means that this filter is inconclusive
-        if (!attributes.isEmpty() && cache.getAttributes().isEmpty() && !cache.isDetailed()) {
+        if (cache.getAttributes().isEmpty() && !cache.isDetailed()) {
             return null;
         }
 
         int found = 0;
         for (String cacheAtt : cache.getAttributes()) {
-            if (attributes.contains(cacheAtt)) {
+            if (attributes.containsKey(cacheAtt) || attributesRaw.contains(cacheAtt)) {
                 found++;
             }
         }
@@ -73,9 +84,14 @@ public class AttributesGeocacheFilter extends BaseGeocacheFilter {
                 sqlBuilder.openWhere(SqlBuilder.WhereType.NOT);
             }
             sqlBuilder.openWhere(SqlBuilder.WhereType.AND);
-            for (String att : attributes) {
+            for (Map.Entry<String, String> att : attributes.entrySet()) {
                 final String attTableId = sqlBuilder.getNewTableId();
-                sqlBuilder.addWhere("EXISTS (SELECT geocode FROM cg_attributes " + attTableId + " WHERE " + attTableId + ".geocode = " + sqlBuilder.getMainTableId() + ".geocode AND attribute = ?)", att);
+                final String whereStart = "EXISTS (SELECT geocode FROM cg_attributes " + attTableId + " WHERE " + attTableId + ".geocode = " + sqlBuilder.getMainTableId() + ".geocode AND attribute ";
+                if (att.getValue() == null) {
+                    sqlBuilder.addWhere(whereStart + " = ?)", att.getKey());
+                } else {
+                    sqlBuilder.addWhere(whereStart + " IN (?, ?))", att.getKey(), att.getValue());
+                }
             }
             sqlBuilder.closeWhere();
             if (inverse) {
@@ -88,12 +104,20 @@ public class AttributesGeocacheFilter extends BaseGeocacheFilter {
     @Override
     public void setConfig(final String[] value) {
         attributes.clear();
+        attributesRaw.clear();
         if (value.length == 0) {
             inverse = false;
         } else {
             inverse = BooleanUtils.toBoolean(value[0]);
             for (int idx = 1; idx < value.length; idx++) {
-                attributes.add(value[idx]);
+                final CacheAttribute ca = CacheAttribute.getByName(value[idx]);
+                if (ca != null) {
+                    final boolean isYesValue = CacheAttribute.isEnabled(value[idx]);
+                    attributes.put(value[idx], isYesValue ? ca.rawName : null);
+                    if (isYesValue) {
+                        attributesRaw.add(ca.rawName);
+                    }
+                }
             }
         }
     }
@@ -103,9 +127,21 @@ public class AttributesGeocacheFilter extends BaseGeocacheFilter {
         final String[] result = new String[attributes.size() + 1];
         result[0] = Boolean.toString(inverse);
         int idx = 1;
-        for (String att : attributes) {
+        for (String att : attributes.keySet()) {
             result[idx++] = att;
         }
         return result;
     }
+
+    @Override
+    protected String getUserDisplayableConfig() {
+        if (attributes.isEmpty()) {
+            return LocalizationUtils.getString(R.string.cache_filter_userdisplay_none);
+        }
+        if (attributes.size() > 1) {
+            return LocalizationUtils.getString(R.string.cache_filter_userdisplay_multi);
+        }
+
+        return attributes.keySet().iterator().next();
+    }
 }

fix #8620: filtering for attributes for oc caches (#10646)