├── .travis.yml ├── .gitignore ├── src ├── main │ └── java │ │ └── ch │ │ └── mfrey │ │ └── jackson │ │ └── antpathfilter │ │ ├── AntPathFilterMixin.java │ │ ├── StringUtils.java │ │ ├── Jackson2Helper.java │ │ ├── AntPathPropertyFilter.java │ │ └── AntPathMatcher.java ├── test │ └── java │ │ └── ch │ │ └── mfrey │ │ └── jackson │ │ └── antpathfilter │ │ └── test │ │ ├── Address.java │ │ ├── Judgement.java │ │ ├── AntPathConvertTest.java │ │ ├── ConcurrencyTest.java │ │ ├── User.java │ │ └── AntPathFilterTest.java └── jmh │ └── java │ └── ch │ └── mfrey │ └── jackson │ └── antpathfilter │ └── test │ ├── ComparisonBenchmark.java │ └── FilterBenchmark.java ├── pom.xml └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .settings/ 3 | .externalToolBuilders/ 4 | .idea/ 5 | *.iml 6 | .project 7 | .classpath 8 | /.svn 9 | -------------------------------------------------------------------------------- /src/main/java/ch/mfrey/jackson/antpathfilter/AntPathFilterMixin.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFilter; 4 | 5 | /** 6 | * The Class AntPathFilter. 7 | * 8 | * @author Martin Frey 9 | */ 10 | @JsonFilter("antPathFilter") 11 | public class AntPathFilterMixin { 12 | 13 | } -------------------------------------------------------------------------------- /src/test/java/ch/mfrey/jackson/antpathfilter/test/Address.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | public class Address { 4 | 5 | private String streetName; 6 | 7 | private String streetNumber; 8 | 9 | public String getStreetName() { 10 | return streetName; 11 | } 12 | 13 | public String getStreetNumber() { 14 | return streetNumber; 15 | } 16 | 17 | public void setStreetName(final String streetName) { 18 | this.streetName = streetName; 19 | } 20 | 21 | public void setStreetNumber(final String streetNumber) { 22 | this.streetNumber = streetNumber; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/ch/mfrey/jackson/antpathfilter/test/Judgement.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | import java.util.Date; 4 | 5 | class Judgement { 6 | private int id; 7 | private String judgementNo; 8 | private Date judgementDate; 9 | private Courthouse courthouse; 10 | 11 | public int getId() { 12 | return id; 13 | } 14 | 15 | public void setId(int id) { 16 | this.id = id; 17 | } 18 | 19 | public String getJudgementNo() { 20 | return judgementNo; 21 | } 22 | 23 | public void setJudgementNo(String judgementNo) { 24 | this.judgementNo = judgementNo; 25 | } 26 | 27 | public Date getJudgementDate() { 28 | return judgementDate; 29 | } 30 | 31 | public void setJudgementDate(Date judgementDate) { 32 | this.judgementDate = judgementDate; 33 | } 34 | 35 | public Courthouse getCourthouse() { 36 | return courthouse; 37 | } 38 | 39 | public void setCourthouse(Courthouse courthouse) { 40 | this.courthouse = courthouse; 41 | } 42 | 43 | public static class Courthouse { 44 | private int id; 45 | private String name; 46 | 47 | public String getName() { 48 | return name; 49 | } 50 | 51 | public void setName(String name) { 52 | this.name = name; 53 | } 54 | 55 | public int getId() { 56 | return id; 57 | } 58 | 59 | public void setId(int id) { 60 | this.id = id; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/jmh/java/ch/mfrey/jackson/antpathfilter/test/ComparisonBenchmark.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.openjdk.jmh.annotations.Benchmark; 8 | import org.openjdk.jmh.annotations.BenchmarkMode; 9 | import org.openjdk.jmh.annotations.Fork; 10 | import org.openjdk.jmh.annotations.Measurement; 11 | import org.openjdk.jmh.annotations.Mode; 12 | import org.openjdk.jmh.annotations.OutputTimeUnit; 13 | import org.openjdk.jmh.annotations.Scope; 14 | import org.openjdk.jmh.annotations.State; 15 | import org.openjdk.jmh.annotations.Threads; 16 | import org.openjdk.jmh.annotations.Warmup; 17 | 18 | import com.fasterxml.jackson.core.JsonProcessingException; 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | 21 | import ch.mfrey.jackson.antpathfilter.Jackson2Helper; 22 | 23 | @State(Scope.Benchmark) 24 | @BenchmarkMode(Mode.AverageTime) 25 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 26 | @Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) 27 | @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) 28 | @Fork(3) 29 | @Threads(5) 30 | public class ComparisonBenchmark { 31 | 32 | private final Jackson2Helper jackson2Helper = new Jackson2Helper(); 33 | private final List users = new ArrayList(); 34 | private ObjectMapper objectMapper = new ObjectMapper(); 35 | 36 | public ComparisonBenchmark() { 37 | for (int i = 0; i < 10; i++) { 38 | users.add(User.buildRecursive(100)); 39 | } 40 | } 41 | 42 | @Benchmark 43 | public void measureFiltered() { 44 | jackson2Helper.writeFiltered(users, 45 | "*", "**.manager.*", "**.address.streetName", "**.reports.lastName"); 46 | } 47 | 48 | @Benchmark 49 | public void measureSpecificFilters() { 50 | jackson2Helper.writeFiltered(users, 51 | "firstName", "lastName", "email", "manager.firstName", "manager.lastName"); 52 | } 53 | 54 | @Benchmark 55 | public void measureStandard() throws JsonProcessingException { 56 | objectMapper.writer().writeValueAsString(users); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/ch/mfrey/jackson/antpathfilter/test/AntPathConvertTest.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.List; 6 | 7 | import org.junit.Test; 8 | 9 | import com.fasterxml.jackson.core.JsonProcessingException; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | 12 | import ch.mfrey.jackson.antpathfilter.Jackson2Helper; 13 | import ch.mfrey.jackson.antpathfilter.test.Judgement.Courthouse; 14 | 15 | public class AntPathConvertTest { 16 | private final Jackson2Helper jackson2Helper = new Jackson2Helper(); 17 | 18 | @Test 19 | public void testConvert() throws JsonProcessingException { 20 | List judgements = new ArrayList(); 21 | Judgement judgement = new Judgement(); 22 | judgement.setId(1); 23 | judgement.setJudgementDate(new Date()); 24 | judgement.setJudgementNo("1"); 25 | Courthouse courthouse = new Courthouse(); 26 | courthouse.setId(1); 27 | courthouse.setName("Courthouse 1"); 28 | judgement.setCourthouse(courthouse); 29 | judgements.add(judgement); 30 | 31 | judgement = new Judgement(); 32 | judgement.setId(2); 33 | judgement.setJudgementDate(new Date()); 34 | judgement.setJudgementNo("2"); 35 | courthouse = new Courthouse(); 36 | courthouse.setId(2); 37 | courthouse.setName("Courthouse 2"); 38 | judgement.setCourthouse(courthouse); 39 | judgements.add(judgement); 40 | 41 | String[] includedFieldNames = { "id", "judgementNo", "judgementDate", "courthouse", "courthouse.name", 42 | "@loaded" }; 43 | ObjectMapper mapper = jackson2Helper.buildObjectMapper(includedFieldNames); 44 | 45 | String result = mapper.writeValueAsString(judgements); 46 | System.out.println(result); 47 | 48 | com.fasterxml.jackson.databind.type.CollectionType collectionType = mapper.getTypeFactory() 49 | .constructCollectionType(List.class, Object.class); 50 | 51 | List map = mapper.convertValue(judgements, collectionType); 52 | System.out.println(map); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/ch/mfrey/jackson/antpathfilter/StringUtils.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.StringTokenizer; 7 | 8 | /** 9 | * A takeout of Springs StringUtils functionality just to match the need of AntPathPropertyFilter. 10 | * 11 | * @author Martin Frey 12 | * 13 | * Miscellaneous {@link String} utility methods. 14 | * 15 | *

16 | * Mainly for internal use within the framework; consider Jakarta's Commons Lang for a more comprehensive suite of 18 | * String utilities. 19 | * 20 | *

21 | * This class delivers some simple functionality that should really be provided by the core Java {@code String} 22 | * and {@link StringBuilder} classes, such as the ability to replace all occurrences of a given 23 | * substring in a target string. It also provides easy-to-use methods to convert between delimited strings, such 24 | * as CSV strings, and collections and arrays. 25 | * 26 | * @author Rod Johnson 27 | * @author Juergen Hoeller 28 | * @author Keith Donald 29 | * @author Rob Harrop 30 | * @author Rick Evans 31 | * @author Arjen Poutsma 32 | * @since 16 April 2001 33 | */ 34 | public class StringUtils { 35 | 36 | public static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, 37 | boolean ignoreEmptyTokens) { 38 | 39 | if (str == null) { 40 | return null; 41 | } 42 | StringTokenizer st = new StringTokenizer(str, delimiters); 43 | List tokens = new ArrayList(); 44 | while (st.hasMoreTokens()) { 45 | String token = st.nextToken(); 46 | if (trimTokens) { 47 | token = token.trim(); 48 | } 49 | if (!ignoreEmptyTokens || token.length() > 0) { 50 | tokens.add(token); 51 | } 52 | } 53 | return toStringArray(tokens); 54 | } 55 | 56 | public static boolean hasLength(CharSequence str) { 57 | return (str != null && str.length() > 0); 58 | } 59 | 60 | public static boolean hasText(CharSequence str) { 61 | if (!hasLength(str)) { 62 | return false; 63 | } 64 | int strLen = str.length(); 65 | for (int i = 0; i < strLen; i++) { 66 | if (!Character.isWhitespace(str.charAt(i))) { 67 | return true; 68 | } 69 | } 70 | return false; 71 | } 72 | 73 | public static String[] toStringArray(Collection collection) { 74 | if (collection == null) { 75 | return null; 76 | } 77 | return collection.toArray(new String[collection.size()]); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/ch/mfrey/jackson/antpathfilter/test/ConcurrencyTest.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | import java.util.concurrent.Callable; 4 | import java.util.concurrent.ExecutorService; 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | import org.junit.Assert; 10 | import org.junit.Test; 11 | 12 | import ch.mfrey.jackson.antpathfilter.Jackson2Helper; 13 | 14 | public class ConcurrencyTest { 15 | 16 | private final class UserRunner implements Callable { 17 | private final AtomicInteger count; 18 | private final User user; 19 | 20 | private UserRunner(User user, AtomicInteger count) { 21 | this.user = user; 22 | this.count = count; 23 | } 24 | 25 | public Object call() throws Exception { 26 | try { 27 | for (int i = 0; i < 30; i++) { 28 | String json = jackson2Helper.writeFiltered(user, "*", i % 2 == 0 ? "!manager" : "!reports", i % 3 == 0 ? "!address" : "!reports"); 29 | if (i % 2 == 0 && i % 3 == 0) { 30 | Assert.assertEquals("{\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"reports\":[{},{},{},{},{},{},{},{},{},{}]}", json); 31 | } else if (i % 2 == 0) { 32 | Assert.assertEquals("{\"address\":{},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\"}", json); 33 | } else if (i % 3 == 0) { 34 | Assert.assertEquals("{\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"manager\":{}}", json); 35 | } else { 36 | Assert.assertEquals("{\"address\":{},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"manager\":{}}", json); 37 | } 38 | } 39 | count.incrementAndGet(); 40 | } catch (Throwable t) { 41 | System.out.println(t.getMessage()); 42 | } 43 | return null; 44 | } 45 | } 46 | 47 | private final Jackson2Helper jackson2Helper = new Jackson2Helper(); 48 | 49 | @Test 50 | public void testConcurrency() throws InterruptedException { 51 | final AtomicInteger count = new AtomicInteger(0); 52 | User user = User.buildMySelf(); 53 | UserRunner runner = new UserRunner(user, count); 54 | 55 | ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); 56 | for (int i = 0; i < 1000; i++) { 57 | fixedThreadPool.submit(runner); 58 | } 59 | fixedThreadPool.shutdown(); 60 | fixedThreadPool.awaitTermination(5, TimeUnit.MINUTES); 61 | Assert.assertEquals(1000, count.intValue()); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/ch/mfrey/jackson/antpathfilter/Jackson2Helper.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; 7 | 8 | /** 9 | * This is just a help for a start. 10 | */ 11 | public class Jackson2Helper { 12 | 13 | private final ObjectMapper objectMapper; 14 | 15 | public Jackson2Helper() { 16 | super(); 17 | this.objectMapper = new ObjectMapper(); 18 | this.getObjectMapper().addMixIn(Object.class, AntPathFilterMixin.class); 19 | } 20 | 21 | /** 22 | * Allows to explicitly override the default ObjectMapper with an own 23 | * instance to be able to add more functionality. It is important to know 24 | * that the given objectMapper needs to contain the 25 | * {@link AntPathFilterMixin}! 26 | * 27 | * @param objectMapper 28 | * The ObjectMapper 29 | */ 30 | public Jackson2Helper(ObjectMapper objectMapper) { 31 | super(); 32 | this.objectMapper = objectMapper; 33 | } 34 | 35 | /** 36 | * Returns a prepared copy of the ObjectMapper with the filters set to the 37 | * current arguments. 38 | * 39 | * @param filters 40 | * The filters to be used 41 | * @return The prepared {@link ObjectMapper} ready for filtering 42 | */ 43 | public ObjectMapper buildObjectMapper(final String... filters) { 44 | ObjectMapper copyForFilter = getObjectMapper().copy(); 45 | copyForFilter.setFilters(buildFilterProvider(filters)); 46 | return copyForFilter; 47 | } 48 | 49 | /** 50 | * Build the FilterProvider for the given filters 51 | * 52 | * @param filters 53 | * The filters to be user 54 | * @return The configured FilterProvider 55 | */ 56 | public SimpleFilterProvider buildFilterProvider(final String... filters) { 57 | return new SimpleFilterProvider().addFilter("antPathFilter", new AntPathPropertyFilter(filters)); 58 | } 59 | 60 | /** 61 | * Convenience method to simply write an object to a json representation 62 | * using the given filters. 63 | * 64 | * @param value 65 | * Any object that can be serialized to json 66 | * @param filters 67 | * The desired filters to be used 68 | * @return The json representation 69 | */ 70 | public String writeFiltered(final Object value, final String... filters) { 71 | try { 72 | return buildObjectMapper(filters).writeValueAsString(value); 73 | } catch (IOException ioe) { 74 | throw new RuntimeException("Could not write object filtered.", ioe); 75 | } 76 | } 77 | 78 | /** 79 | * Get the ObjectMapper 80 | * 81 | * @return The ObjectMapper 82 | */ 83 | public ObjectMapper getObjectMapper() { 84 | return objectMapper; 85 | } 86 | } -------------------------------------------------------------------------------- /src/jmh/java/ch/mfrey/jackson/antpathfilter/test/FilterBenchmark.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import org.openjdk.jmh.annotations.Benchmark; 12 | import org.openjdk.jmh.annotations.BenchmarkMode; 13 | import org.openjdk.jmh.annotations.Fork; 14 | import org.openjdk.jmh.annotations.Measurement; 15 | import org.openjdk.jmh.annotations.Mode; 16 | import org.openjdk.jmh.annotations.OutputTimeUnit; 17 | import org.openjdk.jmh.annotations.Scope; 18 | import org.openjdk.jmh.annotations.State; 19 | import org.openjdk.jmh.annotations.Threads; 20 | import org.openjdk.jmh.annotations.Warmup; 21 | 22 | import com.fasterxml.jackson.core.JsonProcessingException; 23 | import com.fasterxml.jackson.databind.ObjectMapper; 24 | import com.fasterxml.jackson.databind.ObjectWriter; 25 | 26 | import ch.mfrey.jackson.antpathfilter.Jackson2Helper; 27 | 28 | @State(Scope.Benchmark) 29 | @BenchmarkMode(Mode.AverageTime) 30 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 31 | @Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) 32 | @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) 33 | @Fork(3) 34 | @Threads(5) 35 | public class FilterBenchmark { 36 | 37 | private final Jackson2Helper jackson2Helper = new Jackson2Helper(); 38 | private final List users = new ArrayList(); 39 | 40 | private final Map cache = new ConcurrentHashMap(); 41 | 42 | /** 43 | * Convenience method to simply write an object to a json representation 44 | * using the given filters. 45 | * 46 | * @param value 47 | * Any object that can be serialized to json 48 | * @param filters 49 | * The desired filters to be used 50 | * @return The json representation 51 | */ 52 | public String writeCachedFiltered(final Object value, final String... filters) { 53 | try { 54 | int key = Arrays.hashCode(filters); 55 | if (!cache.containsKey(key)) { 56 | cache.put(key, jackson2Helper.buildObjectMapper(filters)); 57 | } 58 | return cache.get(key).writeValueAsString(value); 59 | } catch (IOException ioe) { 60 | throw new RuntimeException("Could not write object filtered.", ioe); 61 | } 62 | } 63 | 64 | public FilterBenchmark() { 65 | for (int i = 0; i < 1000; i++) { 66 | users.add(User.buildMySelf()); 67 | } 68 | } 69 | 70 | @Benchmark 71 | public void measureCached() { 72 | for (int i = 0; i < 100; i++) { 73 | writeCachedFiltered(users, "*", i % 2 == 0 ? "-manager" : "-reports", i % 3 == 0 ? "-address" : "-reports"); 74 | } 75 | } 76 | 77 | @Benchmark 78 | public void measureNotCached() { 79 | for (int i = 0; i < 100; i++) { 80 | jackson2Helper.writeFiltered(users, "*", i % 2 == 0 ? "-manager" : "-reports", 81 | i % 3 == 0 ? "-address" : "-reports"); 82 | } 83 | } 84 | 85 | @Benchmark 86 | public void measureWriter() throws JsonProcessingException { 87 | for (int i = 0; i < 100; i++) { 88 | ObjectWriter writer = jackson2Helper.getObjectMapper() 89 | .writer(jackson2Helper.buildFilterProvider("*", 90 | i % 2 == 0 ? "-manager" : "-reports", 91 | i % 3 == 0 ? "-address" : "-reports")); 92 | writer.writeValueAsString(users); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/ch/mfrey/jackson/antpathfilter/test/User.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class User { 7 | public static User buildMySelf() { 8 | User myself = new User(); 9 | myself.setFirstName("Martin"); 10 | myself.setLastName("Frey"); 11 | myself.setEmail("somewhere@no.where"); 12 | Address address = new Address(); 13 | address.setStreetName("At my place"); 14 | address.setStreetNumber("1"); 15 | myself.setAddress(address); 16 | 17 | User manager = new User(); 18 | manager.setFirstName("John"); 19 | manager.setLastName("Doe"); 20 | manager.setEmail("john.doe@no.where"); 21 | myself.setManager(manager); 22 | 23 | myself.setReports(new ArrayList()); 24 | for (int i = 0; i < 10; i++) { 25 | final User report = new User(); 26 | report.setFirstName("First " + i); 27 | report.setLastName("Doe " + i); 28 | report.setEmail("report" + i + "@no.where"); 29 | myself.getReports().add(report); 30 | } 31 | 32 | return myself; 33 | } 34 | 35 | public static User buildRecursive(int lvl) { 36 | User myself = new User(); 37 | myself.setFirstName("Martin"); 38 | myself.setLastName("Frey"); 39 | myself.setEmail("somewhere@no.where"); 40 | Address address = new Address(); 41 | address.setStreetName("At my place"); 42 | address.setStreetNumber("1"); 43 | myself.setAddress(address); 44 | 45 | myself.setReports(new ArrayList()); 46 | for (int i = 0; i < 10; i++) { 47 | final User report = new User(); 48 | report.setFirstName("First " + i); 49 | report.setLastName("Doe " + i); 50 | report.setEmail("report" + i + "@no.where"); 51 | myself.getReports().add(report); 52 | } 53 | 54 | if (lvl > 0) { 55 | myself.setManager(buildRecursive(lvl - 1)); 56 | } 57 | return myself; 58 | } 59 | 60 | public static User buildRecursive() { 61 | User myself = new User(); 62 | myself.setFirstName("Martin"); 63 | myself.setLastName("Frey"); 64 | myself.setEmail("somewhere@no.where"); 65 | Address address = new Address(); 66 | address.setStreetName("At my place"); 67 | address.setStreetNumber("1"); 68 | myself.setAddress(address); 69 | 70 | myself.setManager(myself); 71 | return myself; 72 | } 73 | 74 | private Address address; 75 | 76 | private String email; 77 | 78 | private String firstName; 79 | 80 | private String lastName; 81 | 82 | private User manager; 83 | 84 | private List reports; 85 | 86 | public Address getAddress() { 87 | return address; 88 | } 89 | 90 | public String getEmail() { 91 | return email; 92 | } 93 | 94 | public String getFirstName() { 95 | return firstName; 96 | } 97 | 98 | public String getLastName() { 99 | return lastName; 100 | } 101 | 102 | public User getManager() { 103 | return manager; 104 | } 105 | 106 | public List getReports() { 107 | return reports; 108 | } 109 | 110 | public void setAddress(final Address address) { 111 | this.address = address; 112 | } 113 | 114 | public void setEmail(final String email) { 115 | this.email = email; 116 | } 117 | 118 | public void setFirstName(final String firstName) { 119 | this.firstName = firstName; 120 | } 121 | 122 | public void setLastName(final String lastName) { 123 | this.lastName = lastName; 124 | } 125 | 126 | public void setManager(final User manager) { 127 | this.manager = manager; 128 | } 129 | 130 | public void setReports(List reports) { 131 | this.reports = reports; 132 | } 133 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | ch.mfrey.jackson 4 | jackson-antpathfilter 5 | 1.0.3-SNAPSHOT 6 | Jackson AntPath Filter 7 | An implementation to add filtering based on AntPath matching 8 | https://github.com/Antibrumm/jackson-antpathfilter 9 | 10 | 11 | The Apache Software License, Version 2.0 12 | http://www.apache.org/licenses/LICENSE-2.0.txt 13 | repo 14 | 15 | 16 | 17 | 18 | org.sonatype.oss 19 | oss-parent 20 | 7 21 | 22 | 23 | 24 | 3.0.0 25 | 26 | 27 | 28 | scm:git:git@github.com:Antibrumm/jackson-antpathfilter.git 29 | https://github.com/Antibrumm/jackson-antpathfilter.git 30 | scm:git:git@github.com:Antibrumm/jackson-antpathfilter.git 31 | 32 | 33 | 34 | 35 | Martin Frey 36 | 37 | Project Admin 38 | Lead Developer 39 | 40 | 41 | 42 | 43 | 44 | 45 | https://github.com/Antibrumm/jackson-antpathfilter.git 46 | 47 | 48 | 49 | 1.5 50 | UTF-8 51 | 2.11.2 52 | 53 | 54 | 55 | 56 | com.fasterxml.jackson.core 57 | jackson-core 58 | ${jackson-version} 59 | 60 | 61 | com.fasterxml.jackson.core 62 | jackson-databind 63 | ${jackson-version} 64 | 65 | 66 | junit 67 | junit 68 | 4.13.1 69 | test 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-compiler-plugin 78 | 2.3.2 79 | 80 | true 81 | ${java-version} 82 | ${java-version} 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | benchmark 92 | 93 | 1.11.1 94 | 95 | 96 | 97 | org.openjdk.jmh 98 | jmh-core 99 | ${jmh-version} 100 | provided 101 | 102 | 103 | org.openjdk.jmh 104 | jmh-generator-annprocess 105 | ${jmh-version} 106 | provided 107 | 108 | 109 | 110 | 111 | 112 | org.codehaus.mojo 113 | build-helper-maven-plugin 114 | 115 | 116 | generate-test-sources 117 | 118 | add-test-source 119 | 120 | 121 | 122 | src/jmh/java 123 | 124 | 125 | 126 | 127 | 128 | 129 | org.codehaus.mojo 130 | exec-maven-plugin 131 | 132 | 133 | run-benchmarks 134 | test 135 | 136 | exec 137 | 138 | 139 | test 140 | java 141 | 142 | -classpath 143 | 144 | org.openjdk.jmh.Main 145 | .* 146 | h 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Jackson AntPath Property Filter 3 | =============================== 4 | 5 | A Jackson Filter matching the path of the current value to serialize against the AntPathMatcher. The inclusion / exclusion works similar to the `ant` file `include / exclude` functionality. Ant / Maven users should mostly be aware of how this works. 6 | 7 | [![Build Status](https://travis-ci.org/Antibrumm/jackson-antpathfilter.png)](https://travis-ci.org/Antibrumm/jackson-antpathfilter) 8 | 9 | Requirements 10 | ------------ 11 | 12 | - Java 5 13 | - Jackson 2.5.0+ (2.5.0.RELEASE and its dependencies included) 14 | 15 | 16 | Installation 17 | ------------ 18 | 19 | ### For Maven and Maven-compatible dependency managers 20 | Add a dependency to your project with the following co-ordinates: 21 | 22 | - GroupId: `ch.mfrey.jackson` 23 | - ArtifactId: `jackson-antpathfilter` 24 | - Version: `${jackson-antpathfilter.version}` 25 | 26 | ```xml 27 | 28 | ch.mfrey.jackson 29 | jackson-antpathfilter 30 | ${jackson-antpathfilter.version} 31 | 32 | ``` 33 | 34 | Usage 35 | ----- 36 | 37 | ```java 38 | String[] filter = new String[] {"*", "*.*", "!not.that.path"}; 39 | 40 | ObjectMapper objectMapper = new ObjectMapper(); 41 | objectMapper.addMixIn(Object.class, AntPathFilterMixin.class); 42 | 43 | FilterProvider filterProvider = new SimpleFilterProvider().addFilter("antPathFilter", new AntPathPropertyFilter(filter)); 44 | objectMapper.setFilters(filterProvider); 45 | 46 | objectMapper.writeValueAsString(someObject); 47 | ``` 48 | 49 | = Inclusion: 50 | 51 | ``` 52 | "*", "**", "*.*", "someproperty.someNesterProperty.*", 53 | ``` 54 | 55 | = Exclusion: 56 | 57 | ``` 58 | "!property", "!**.someExpensiveMethod"; 59 | ``` 60 | 61 | Spring Integration 62 | ------------------ 63 | 64 | = With Spring 4.2.2+ 65 | 66 | ```java 67 | public class AntPathMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { 68 | 69 | public AntPathMappingJackson2HttpMessageConverter(ObjectMapper originalObjectMapper) { 70 | super(originalObjectMapper.copy().addMixIn(Object.class, HibernateAwareAntPathFilterMixin.class)); 71 | } 72 | 73 | @Override 74 | public boolean canWrite(Class clazz, MediaType mediaType) { 75 | return AntPathFilterMappingJacksonValue.class.isAssignableFrom(clazz); 76 | } 77 | 78 | @JsonFilter("antPathFilter") 79 | @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 80 | public static class HibernateAwareAntPathFilterMixin { 81 | } 82 | } 83 | 84 | public class AntPathFilterMappingJacksonValue extends MappingJacksonValue { 85 | 86 | public AntPathFilterMappingJacksonValue(final T value, final String... filters) { 87 | super(value); 88 | setFilters(new SimpleFilterProvider().addFilter("antPathFilter", new AntPathPropertyFilter(filters))); 89 | } 90 | } 91 | ``` 92 | 93 | ```java 94 | @Configuration 95 | @EnableWebMvc 96 | public class DispatcherConfiguration extends WebMvcConfigurerAdapter { 97 | 98 | @Bean 99 | public FactoryBean jacksonObjectMapperFactory() { 100 | // Normal Jackson configuration 101 | } 102 | 103 | @Bean 104 | public HttpMessageConverter antpathJacksonConverter() { 105 | return new AntPathMappingJackson2HttpMessageConverter(jacksonObjectMapperFactory().getObject()); 106 | } 107 | 108 | @Override 109 | public void extendMessageConverters(List> converters) { 110 | converters.add(0, antpathJacksonConverter()); 111 | } 112 | } 113 | ``` 114 | 115 | ```java 116 | @Controller 117 | @RequestMapping(value = "/someObject") 118 | public class SomeController { 119 | 120 | @RequestMapping 121 | @ResponseBody 122 | public AntPathFilterMappingJacksonValue getSomeObject() { 123 | return new AntPathFilterMappingJacksonValue<>(someObject, "*", "*.*", "!not.that.path"); 124 | } 125 | } 126 | ``` 127 | 128 | Examples (from the Unit Tests) 129 | ------------------------------ 130 | 131 | Data 132 | ``` 133 | { 134 | "address": { 135 | "streetName":"At my place", 136 | "streetNumber":"1" 137 | }, 138 | "email":"somewhere@no.where", 139 | "firstName":"Martin", 140 | "lastName":"Frey", 141 | "manager":{ 142 | "address":null, 143 | "email":"john.doe@no.where", 144 | "firstName":"John", 145 | "lastName":"Doe", 146 | "manager":null 147 | } 148 | } 149 | 150 | ``` 151 | 152 | ``` 153 | Filter: **, 154 | Result: {"address":{"streetName":"At my place","streetNumber":"1"},"email":"somewhere@no.where","firstName":"Martin","lastName":"Frey","manager":{"address":null,"email":"john.doe@no.where","firstName":"John","lastName":"Doe","manager":null}} 155 | ``` 156 | 157 | ``` 158 | Filter: firstName, 159 | Result: {"firstName":"Martin"} 160 | ``` 161 | 162 | ``` 163 | Filter: **,!manager, 164 | Result: {"address":{"streetName":"At my place","streetNumber":"1"},"email":"somewhere@no.where","firstName":"Martin","lastName":"Frey"} 165 | ``` 166 | 167 | ``` 168 | Filter: manager,manager.firstName,manager.lastName, 169 | Result: {"manager":{"firstName":"John","lastName":"Doe"}} 170 | ``` 171 | 172 | ``` 173 | Filter: *,address.*,manager.firstName, 174 | Result: {"address":{"streetName":"At my place","streetNumber":"1"},"email":"somewhere@no.where","firstName":"Martin","lastName":"Frey","manager":{"firstName":"John"}} 175 | ``` 176 | 177 | ``` 178 | Filter: **,-manager,!**.streetNumber, 179 | Result: {"address":{"streetName":"At my place"},"email":"somewhere@no.where","firstName":"Martin","lastName":"Frey"} 180 | ``` 181 | 182 | ``` 183 | Filter: "reports", "reports.firstName" 184 | Result: {"reports":[{"firstName":"First 0"},{"firstName":"First 1"},{"firstName":"First 2"},{"firstName":"First 3"},{"firstName":"First 4"},{"firstName":"First 5"},{"firstName":"First 6"},{"firstName":"First 7"},{"firstName":"First 8"},{"firstName":"First 9"}]} 185 | ``` 186 | -------------------------------------------------------------------------------- /src/main/java/ch/mfrey/jackson/antpathfilter/AntPathPropertyFilter.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import java.util.logging.Logger; 8 | 9 | import com.fasterxml.jackson.core.JsonGenerator; 10 | import com.fasterxml.jackson.core.JsonStreamContext; 11 | import com.fasterxml.jackson.databind.SerializerProvider; 12 | import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; 13 | import com.fasterxml.jackson.databind.ser.PropertyWriter; 14 | import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; 15 | 16 | /** 17 | * Implementation that allows to set nested properties. The filter will use the 18 | * parents from the context to identify if a property has to be filtered. 19 | * 20 | * Example: user -> manager (user) 21 | * 22 | * "id", "firstName", "lastName", "manager.id", "manager.fullName" 23 | * 24 | * { "id" : "2", "firstName" : "Martin", "lastName" : "Frey", manager : { "id" : 25 | * "1", "fullName" : "System Administrator"}} 26 | * 27 | * @author Martin Frey 28 | */ 29 | public class AntPathPropertyFilter extends SimpleBeanPropertyFilter { 30 | 31 | private static final Logger log = Logger.getLogger(AntPathPropertyFilter.class.toString()); 32 | 33 | /** The matcher. */ 34 | private static final AntPathMatcher MATCHER = new AntPathMatcher("."); 35 | 36 | /** The _properties to exclude. */ 37 | protected final Set _propertiesToExclude; 38 | 39 | /** 40 | * Set of property names to include. 41 | */ 42 | protected final Set _propertiesToInclude; 43 | 44 | /** 45 | * Cache of patterns to test, and match results 46 | */ 47 | private final Map matchCache = new HashMap(); 48 | 49 | /** 50 | * Instantiates a new ant path property filter. 51 | * 52 | * @param properties 53 | * the properties 54 | */ 55 | public AntPathPropertyFilter(final String... properties) { 56 | super(); 57 | _propertiesToInclude = new HashSet(properties.length); 58 | _propertiesToExclude = new HashSet(properties.length); 59 | for (int i = 0; i < properties.length; i++) { 60 | if (properties[i].startsWith("-")) { 61 | _propertiesToExclude.add(properties[i].substring(1)); 62 | log.warning("Using '-' for exclusion is now deprecated. Please use '!'"); 63 | } else if (properties[i].startsWith("!")) { 64 | _propertiesToExclude.add(properties[i].substring(1)); 65 | } else { 66 | _propertiesToInclude.add(properties[i]); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Gets the path to test. 73 | * 74 | * @param writer 75 | * the writer 76 | * @param jgen 77 | * the jgen 78 | * @return the path to test 79 | */ 80 | private String getPathToTest(final PropertyWriter writer, final JsonGenerator jgen) { 81 | StringBuilder nestedPath = new StringBuilder(); 82 | nestedPath.append(writer.getName()); 83 | JsonStreamContext sc = jgen.getOutputContext(); 84 | if (sc != null) { 85 | sc = sc.getParent(); 86 | } 87 | while (sc != null) { 88 | if (sc.getCurrentName() != null) { 89 | if (nestedPath.length() > 0) { 90 | nestedPath.insert(0, "."); 91 | } 92 | nestedPath.insert(0, sc.getCurrentName()); 93 | } 94 | sc = sc.getParent(); 95 | } 96 | return nestedPath.toString(); 97 | } 98 | 99 | /* 100 | * (non-Javadoc) 101 | * 102 | * @see 103 | * com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include( 104 | * com.fasterxml.jackson.databind.ser. BeanPropertyWriter) 105 | */ 106 | @Override 107 | protected boolean include(final BeanPropertyWriter writer) { 108 | throw new UnsupportedOperationException("Cannot call include without JsonGenerator"); 109 | } 110 | 111 | /* 112 | * (non-Javadoc) 113 | * 114 | * @see 115 | * com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include( 116 | * com.fasterxml.jackson.databind.ser. PropertyWriter) 117 | */ 118 | @Override 119 | protected boolean include(final PropertyWriter writer) { 120 | throw new UnsupportedOperationException("Cannot call include without JsonGenerator"); 121 | } 122 | 123 | /** 124 | * Include. 125 | * 126 | * @param writer 127 | * the writer 128 | * @param jgen 129 | * the jgen 130 | * @return true, if successful 131 | */ 132 | protected boolean include(final PropertyWriter writer, final JsonGenerator jgen) { 133 | String pathToTest = getPathToTest(writer, jgen); 134 | // Check cache first 135 | if (matchCache.containsKey(pathToTest)) { 136 | return matchCache.get(pathToTest); 137 | } 138 | 139 | // Only Excludes. 140 | if (_propertiesToInclude.isEmpty()) { 141 | for (String pattern : _propertiesToExclude) { 142 | if (matchPath(pathToTest, pattern)) { 143 | matchCache.put(pathToTest, false); 144 | return false; 145 | } 146 | } 147 | matchCache.put(pathToTest, true); 148 | return true; 149 | } 150 | 151 | // Else do full check 152 | boolean include = false; 153 | // Check Includes first 154 | for (String pattern : _propertiesToInclude) { 155 | if (matchPath(pathToTest, pattern)) { 156 | include = true; 157 | break; 158 | } 159 | } 160 | 161 | // Might still be excluded 162 | if (include && !_propertiesToExclude.isEmpty()) { 163 | for (String pattern : _propertiesToExclude) { 164 | if (matchPath(pathToTest, pattern)) { 165 | include = false; 166 | break; 167 | } 168 | } 169 | } 170 | 171 | matchCache.put(pathToTest, include); 172 | return include; 173 | } 174 | 175 | /** 176 | * Only uses AntPathMatcher if the pattern contains wildcards, else use 177 | * simple equals 178 | * 179 | * @param pathToTest 180 | * @param pattern 181 | * @return 182 | */ 183 | private boolean matchPath(String pathToTest, String pattern) { 184 | if (pattern.contains("*")) { 185 | return MATCHER.match(pattern, pathToTest); 186 | } else { 187 | return pattern.equals(pathToTest); 188 | } 189 | } 190 | 191 | /* 192 | * (non-Javadoc) 193 | * 194 | * @see com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter# 195 | * serializeAsField(java.lang.Object, 196 | * com.fasterxml.jackson.core.JsonGenerator, 197 | * com.fasterxml.jackson.databind.SerializerProvider, 198 | * com.fasterxml.jackson.databind.ser.PropertyWriter) 199 | */ 200 | @Override 201 | public void serializeAsField(final Object pojo, final JsonGenerator jgen, final SerializerProvider provider, 202 | final PropertyWriter writer) throws Exception { 203 | 204 | if (include(writer, jgen)) { 205 | writer.serializeAsField(pojo, jgen, provider); 206 | } else if (!jgen.canOmitFields()) { // since 2.3 207 | writer.serializeAsOmittedField(pojo, jgen, provider); 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /src/test/java/ch/mfrey/jackson/antpathfilter/test/AntPathFilterTest.java: -------------------------------------------------------------------------------- 1 | package ch.mfrey.jackson.antpathfilter.test; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import com.fasterxml.jackson.core.JsonProcessingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | 9 | import ch.mfrey.jackson.antpathfilter.Jackson2Helper; 10 | 11 | public class AntPathFilterTest { 12 | private final Jackson2Helper jackson2Helper = new Jackson2Helper(); 13 | 14 | private void assertAntFilter(final Object testObj, final String[] filters, final String outcome) 15 | throws JsonProcessingException { 16 | ObjectMapper objectMapper = jackson2Helper.buildObjectMapper(filters); 17 | String json = objectMapper.writeValueAsString(testObj); 18 | 19 | System.out.print("Filter: "); 20 | for (int i = 0; i < filters.length; i++) { 21 | System.out.print(filters[i] + ","); 22 | } 23 | System.out.println(); 24 | 25 | System.out.println("Result: " + json); 26 | 27 | Assert.assertEquals(outcome, json); 28 | } 29 | 30 | private void assertAntFilter(final String[] filters, final String outcome) throws JsonProcessingException { 31 | assertAntFilter(User.buildMySelf(), filters, outcome); 32 | } 33 | 34 | @Test 35 | public void testExclusion() throws JsonProcessingException { 36 | String[] filters = new String[] { "**", "!manager" }; 37 | assertAntFilter(filters, 38 | "{\"address\":{\"streetName\":\"At my place\",\"streetNumber\":\"1\"},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"reports\":[{\"address\":null,\"email\":\"report0@no.where\",\"firstName\":\"First 0\",\"lastName\":\"Doe 0\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report1@no.where\",\"firstName\":\"First 1\",\"lastName\":\"Doe 1\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report2@no.where\",\"firstName\":\"First 2\",\"lastName\":\"Doe 2\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report3@no.where\",\"firstName\":\"First 3\",\"lastName\":\"Doe 3\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report4@no.where\",\"firstName\":\"First 4\",\"lastName\":\"Doe 4\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report5@no.where\",\"firstName\":\"First 5\",\"lastName\":\"Doe 5\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report6@no.where\",\"firstName\":\"First 6\",\"lastName\":\"Doe 6\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report7@no.where\",\"firstName\":\"First 7\",\"lastName\":\"Doe 7\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report8@no.where\",\"firstName\":\"First 8\",\"lastName\":\"Doe 8\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report9@no.where\",\"firstName\":\"First 9\",\"lastName\":\"Doe 9\",\"manager\":null,\"reports\":null}]}"); 39 | } 40 | 41 | @Test 42 | public void testOldExclusionPattern() throws JsonProcessingException { 43 | String[] filters = new String[] { "**", "-manager" }; 44 | assertAntFilter(filters, 45 | "{\"address\":{\"streetName\":\"At my place\",\"streetNumber\":\"1\"},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"reports\":[{\"address\":null,\"email\":\"report0@no.where\",\"firstName\":\"First 0\",\"lastName\":\"Doe 0\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report1@no.where\",\"firstName\":\"First 1\",\"lastName\":\"Doe 1\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report2@no.where\",\"firstName\":\"First 2\",\"lastName\":\"Doe 2\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report3@no.where\",\"firstName\":\"First 3\",\"lastName\":\"Doe 3\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report4@no.where\",\"firstName\":\"First 4\",\"lastName\":\"Doe 4\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report5@no.where\",\"firstName\":\"First 5\",\"lastName\":\"Doe 5\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report6@no.where\",\"firstName\":\"First 6\",\"lastName\":\"Doe 6\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report7@no.where\",\"firstName\":\"First 7\",\"lastName\":\"Doe 7\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report8@no.where\",\"firstName\":\"First 8\",\"lastName\":\"Doe 8\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report9@no.where\",\"firstName\":\"First 9\",\"lastName\":\"Doe 9\",\"manager\":null,\"reports\":null}]}"); 46 | } 47 | 48 | @Test 49 | public void testExclusion2() throws JsonProcessingException { 50 | String[] filters = new String[] { "**", "!manager", "!**.streetNumber" }; 51 | assertAntFilter(filters, 52 | "{\"address\":{\"streetName\":\"At my place\"},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"reports\":[{\"address\":null,\"email\":\"report0@no.where\",\"firstName\":\"First 0\",\"lastName\":\"Doe 0\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report1@no.where\",\"firstName\":\"First 1\",\"lastName\":\"Doe 1\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report2@no.where\",\"firstName\":\"First 2\",\"lastName\":\"Doe 2\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report3@no.where\",\"firstName\":\"First 3\",\"lastName\":\"Doe 3\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report4@no.where\",\"firstName\":\"First 4\",\"lastName\":\"Doe 4\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report5@no.where\",\"firstName\":\"First 5\",\"lastName\":\"Doe 5\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report6@no.where\",\"firstName\":\"First 6\",\"lastName\":\"Doe 6\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report7@no.where\",\"firstName\":\"First 7\",\"lastName\":\"Doe 7\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report8@no.where\",\"firstName\":\"First 8\",\"lastName\":\"Doe 8\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report9@no.where\",\"firstName\":\"First 9\",\"lastName\":\"Doe 9\",\"manager\":null,\"reports\":null}]}"); 53 | } 54 | 55 | @Test 56 | public void testFirstName() throws JsonProcessingException { 57 | String[] filters = new String[] { "firstName" }; 58 | assertAntFilter(filters, "{\"firstName\":\"Martin\"}"); 59 | } 60 | 61 | @Test 62 | public void testFull() throws JsonProcessingException { 63 | String[] filters = new String[] { "**" }; 64 | assertAntFilter(filters, 65 | "{\"address\":{\"streetName\":\"At my place\",\"streetNumber\":\"1\"},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"manager\":{\"address\":null,\"email\":\"john.doe@no.where\",\"firstName\":\"John\",\"lastName\":\"Doe\",\"manager\":null,\"reports\":null},\"reports\":[{\"address\":null,\"email\":\"report0@no.where\",\"firstName\":\"First 0\",\"lastName\":\"Doe 0\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report1@no.where\",\"firstName\":\"First 1\",\"lastName\":\"Doe 1\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report2@no.where\",\"firstName\":\"First 2\",\"lastName\":\"Doe 2\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report3@no.where\",\"firstName\":\"First 3\",\"lastName\":\"Doe 3\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report4@no.where\",\"firstName\":\"First 4\",\"lastName\":\"Doe 4\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report5@no.where\",\"firstName\":\"First 5\",\"lastName\":\"Doe 5\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report6@no.where\",\"firstName\":\"First 6\",\"lastName\":\"Doe 6\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report7@no.where\",\"firstName\":\"First 7\",\"lastName\":\"Doe 7\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report8@no.where\",\"firstName\":\"First 8\",\"lastName\":\"Doe 8\",\"manager\":null,\"reports\":null},{\"address\":null,\"email\":\"report9@no.where\",\"firstName\":\"First 9\",\"lastName\":\"Doe 9\",\"manager\":null,\"reports\":null}]}"); 66 | } 67 | 68 | @Test 69 | public void testInclusion() throws JsonProcessingException { 70 | String[] filters = new String[] { "*", "address.*", "manager.firstName" }; 71 | assertAntFilter(filters, 72 | "{\"address\":{\"streetName\":\"At my place\",\"streetNumber\":\"1\"},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"manager\":{\"firstName\":\"John\"},\"reports\":[{},{},{},{},{},{},{},{},{},{}]}"); 73 | } 74 | 75 | @Test 76 | public void testManagerNames() throws JsonProcessingException { 77 | String[] filters = new String[] { "manager", "manager.firstName", "manager.lastName" }; 78 | assertAntFilter(filters, "{\"manager\":{\"firstName\":\"John\",\"lastName\":\"Doe\"}}"); 79 | } 80 | 81 | @Test 82 | public void testRecursive1Levels() throws JsonProcessingException { 83 | String[] filters = new String[] { "**", "!manager" }; 84 | assertAntFilter(User.buildRecursive(), filters, 85 | "{\"address\":{\"streetName\":\"At my place\",\"streetNumber\":\"1\"},\"email\":\"somewhere@no.where\",\"firstName\":\"Martin\",\"lastName\":\"Frey\",\"reports\":null}"); 86 | } 87 | 88 | @Test 89 | public void testReports() throws JsonProcessingException { 90 | String[] filters = new String[] { "reports", "reports.firstName" }; 91 | assertAntFilter(User.buildMySelf(), filters, 92 | "{\"reports\":[{\"firstName\":\"First 0\"},{\"firstName\":\"First 1\"},{\"firstName\":\"First 2\"},{\"firstName\":\"First 3\"},{\"firstName\":\"First 4\"},{\"firstName\":\"First 5\"},{\"firstName\":\"First 6\"},{\"firstName\":\"First 7\"},{\"firstName\":\"First 8\"},{\"firstName\":\"First 9\"}]}"); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/ch/mfrey/jackson/antpathfilter/AntPathMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ch.mfrey.jackson.antpathfilter; 18 | 19 | import java.util.Comparator; 20 | import java.util.LinkedHashMap; 21 | import java.util.LinkedList; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.concurrent.ConcurrentHashMap; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | 28 | /** 29 | * 30 | * Kindly borrowed from Spring to provide this feature to Jackson for property filtering. 31 | * 32 | * @author Martin Frey 33 | * 34 | * PathMatcher implementation for Ant-style path patterns. Examples are provided below. 35 | * 36 | *

37 | * Part of this mapping code has been kindly borrowed from Apache Ant. 38 | * 39 | *

40 | * The mapping matches URLs using the following rules:
41 | *

    42 | *
  • ? matches one character
  • 43 | *
  • * matches zero or more characters
  • 44 | *
  • ** matches zero or more 'directories' in a path
  • 45 | *
46 | * 47 | *

48 | * Some examples:
49 | *

    50 | *
  • {@code com/t?st.jsp} - matches {@code com/test.jsp} but also {@code com/tast.jsp} or 51 | * {@code com/txst.jsp}
  • 52 | *
  • {@code com/*.jsp} - matches all {@code .jsp} files in the {@code com} directory
  • 53 | *
  • {@code com/**/test.jsp} - matches all {@code test.jsp} files underneath the {@code com} path
  • 54 | *
  • {@code org/springframework/**/*.jsp} - matches all {@code .jsp} files underneath the 55 | * {@code org/springframework} path
  • 56 | *
  • {@code org/**/servlet/bla.jsp} - matches {@code org/springframework/servlet/bla.jsp} but also 57 | * {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}
  • 58 | *
59 | * 60 | * @author Alef Arendsen 61 | * @author Juergen Hoeller 62 | * @author Rob Harrop 63 | * @author Arjen Poutsma 64 | * @author Rossen Stoyanchev 65 | * @since 16.07.2003 66 | */ 67 | public class AntPathMatcher { 68 | 69 | /** Default path separator: "/" */ 70 | public static final String DEFAULT_PATH_SEPARATOR = "/"; 71 | 72 | private static final int CACHE_TURNOFF_THRESHOLD = 65536; 73 | 74 | private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}"); 75 | 76 | private String pathSeparator; 77 | 78 | private PathSeparatorPatternCache pathSeparatorPatternCache; 79 | 80 | private boolean trimTokens = true; 81 | 82 | private volatile Boolean cachePatterns; 83 | 84 | private final Map tokenizedPatternCache = new ConcurrentHashMap(256); 85 | 86 | final Map stringMatcherCache = new ConcurrentHashMap( 87 | 256); 88 | 89 | /** 90 | * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. 91 | */ 92 | public AntPathMatcher() { 93 | this.pathSeparator = DEFAULT_PATH_SEPARATOR; 94 | this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); 95 | } 96 | 97 | /** 98 | * A convenience alternative constructor to use with a custom path separator. 99 | * 100 | * @param pathSeparator 101 | * the path separator to use, must not be {@code null}. 102 | * @since 4.1 103 | */ 104 | public AntPathMatcher(String pathSeparator) { 105 | this.pathSeparator = pathSeparator; 106 | this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); 107 | } 108 | 109 | /** 110 | * Set the path separator to use for pattern parsing. 111 | * Default is "/", as in Ant. 112 | */ 113 | public void setPathSeparator(String pathSeparator) { 114 | this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); 115 | this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); 116 | } 117 | 118 | /** 119 | * Specify whether to trim tokenized paths and patterns. 120 | * Default is {@code true}. 121 | */ 122 | public void setTrimTokens(boolean trimTokens) { 123 | this.trimTokens = trimTokens; 124 | } 125 | 126 | /** 127 | * Specify whether to cache parsed pattern metadata for patterns passed 128 | * into this matcher's {@link #match} method. A value of {@code true} activates an unlimited pattern cache; a value 129 | * of {@code false} turns 130 | * the pattern cache off completely. 131 | *

132 | * Default is for the cache to be on, but with the variant to automatically turn it off when encountering too many 133 | * patterns to cache at runtime (the threshold is 65536), assuming that arbitrary permutations of patterns are 134 | * coming in, with little chance for encountering a reoccurring pattern. 135 | * 136 | * @see #getStringMatcher(String) 137 | */ 138 | public void setCachePatterns(boolean cachePatterns) { 139 | this.cachePatterns = cachePatterns; 140 | } 141 | 142 | private void deactivatePatternCache() { 143 | this.cachePatterns = false; 144 | this.tokenizedPatternCache.clear(); 145 | this.stringMatcherCache.clear(); 146 | } 147 | 148 | public boolean isPattern(String path) { 149 | return (path.indexOf('*') != -1 || path.indexOf('?') != -1); 150 | } 151 | 152 | public boolean match(String pattern, String path) { 153 | return doMatch(pattern, path, true, null); 154 | } 155 | 156 | public boolean matchStart(String pattern, String path) { 157 | return doMatch(pattern, path, false, null); 158 | } 159 | 160 | /** 161 | * Actually match the given {@code path} against the given {@code pattern}. 162 | * 163 | * @param pattern 164 | * the pattern to match against 165 | * @param path 166 | * the path String to test 167 | * @param fullMatch 168 | * whether a full pattern match is required (else a pattern match 169 | * as far as the given base path goes is sufficient) 170 | * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't 171 | */ 172 | protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) { 173 | if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { 174 | return false; 175 | } 176 | 177 | String[] pattDirs = tokenizePattern(pattern); 178 | String[] pathDirs = tokenizePath(path); 179 | 180 | int pattIdxStart = 0; 181 | int pattIdxEnd = pattDirs.length - 1; 182 | int pathIdxStart = 0; 183 | int pathIdxEnd = pathDirs.length - 1; 184 | 185 | // Match all elements up to the first ** 186 | while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 187 | String pattDir = pattDirs[pattIdxStart]; 188 | if ("**".equals(pattDir)) { 189 | break; 190 | } 191 | if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { 192 | return false; 193 | } 194 | pattIdxStart++; 195 | pathIdxStart++; 196 | } 197 | 198 | if (pathIdxStart > pathIdxEnd) { 199 | // Path is exhausted, only match if rest of pattern is * or **'s 200 | if (pattIdxStart > pattIdxEnd) { 201 | return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path 202 | .endsWith(this.pathSeparator)); 203 | } 204 | if (!fullMatch) { 205 | return true; 206 | } 207 | if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { 208 | return true; 209 | } 210 | for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 211 | if (!pattDirs[i].equals("**")) { 212 | return false; 213 | } 214 | } 215 | return true; 216 | } else if (pattIdxStart > pattIdxEnd) { 217 | // String not exhausted, but pattern is. Failure. 218 | return false; 219 | } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { 220 | // Path start definitely matches due to "**" part in pattern. 221 | return true; 222 | } 223 | 224 | // up to last '**' 225 | while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 226 | String pattDir = pattDirs[pattIdxEnd]; 227 | if (pattDir.equals("**")) { 228 | break; 229 | } 230 | if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { 231 | return false; 232 | } 233 | pattIdxEnd--; 234 | pathIdxEnd--; 235 | } 236 | if (pathIdxStart > pathIdxEnd) { 237 | // String is exhausted 238 | for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 239 | if (!pattDirs[i].equals("**")) { 240 | return false; 241 | } 242 | } 243 | return true; 244 | } 245 | 246 | while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { 247 | int patIdxTmp = -1; 248 | for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { 249 | if (pattDirs[i].equals("**")) { 250 | patIdxTmp = i; 251 | break; 252 | } 253 | } 254 | if (patIdxTmp == pattIdxStart + 1) { 255 | // '**/**' situation, so skip one 256 | pattIdxStart++; 257 | continue; 258 | } 259 | // Find the pattern between padIdxStart & padIdxTmp in str between 260 | // strIdxStart & strIdxEnd 261 | int patLength = (patIdxTmp - pattIdxStart - 1); 262 | int strLength = (pathIdxEnd - pathIdxStart + 1); 263 | int foundIdx = -1; 264 | 265 | strLoop: for (int i = 0; i <= strLength - patLength; i++) { 266 | for (int j = 0; j < patLength; j++) { 267 | String subPat = pattDirs[pattIdxStart + j + 1]; 268 | String subStr = pathDirs[pathIdxStart + i + j]; 269 | if (!matchStrings(subPat, subStr, uriTemplateVariables)) { 270 | continue strLoop; 271 | } 272 | } 273 | foundIdx = pathIdxStart + i; 274 | break; 275 | } 276 | 277 | if (foundIdx == -1) { 278 | return false; 279 | } 280 | 281 | pattIdxStart = patIdxTmp; 282 | pathIdxStart = foundIdx + patLength; 283 | } 284 | 285 | for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 286 | if (!pattDirs[i].equals("**")) { 287 | return false; 288 | } 289 | } 290 | 291 | return true; 292 | } 293 | 294 | /** 295 | * Tokenize the given path pattern into parts, based on this matcher's settings. 296 | *

297 | * Performs caching based on {@link #setCachePatterns}, delegating to {@link #tokenizePath(String)} for the actual 298 | * tokenization algorithm. 299 | * 300 | * @param pattern 301 | * the pattern to tokenize 302 | * @return the tokenized pattern parts 303 | */ 304 | protected String[] tokenizePattern(String pattern) { 305 | String[] tokenized = null; 306 | Boolean cachePatterns = this.cachePatterns; 307 | if (cachePatterns == null || cachePatterns.booleanValue()) { 308 | tokenized = this.tokenizedPatternCache.get(pattern); 309 | } 310 | if (tokenized == null) { 311 | tokenized = tokenizePath(pattern); 312 | if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { 313 | // Try to adapt to the runtime situation that we're encountering: 314 | // There are obviously too many different patterns coming in here... 315 | // So let's turn off the cache since the patterns are unlikely to be reoccurring. 316 | deactivatePatternCache(); 317 | return tokenized; 318 | } 319 | if (cachePatterns == null || cachePatterns.booleanValue()) { 320 | this.tokenizedPatternCache.put(pattern, tokenized); 321 | } 322 | } 323 | return tokenized; 324 | } 325 | 326 | /** 327 | * Tokenize the given path String into parts, based on this matcher's settings. 328 | * 329 | * @param path 330 | * the path to tokenize 331 | * @return the tokenized path parts 332 | */ 333 | protected String[] tokenizePath(String path) { 334 | return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 335 | } 336 | 337 | /** 338 | * Tests whether or not a string matches against a pattern. 339 | * 340 | * @param pattern 341 | * the pattern to match against (never {@code null}) 342 | * @param str 343 | * the String which must be matched against the pattern (never {@code null}) 344 | * @return {@code true} if the string matches against the pattern, or {@code false} otherwise 345 | */ 346 | private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) { 347 | return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); 348 | } 349 | 350 | /** 351 | * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. 352 | *

353 | * The default implementation checks this AntPathMatcher's internal cache (see {@link #setCachePatterns}), creating 354 | * a new AntPathStringMatcher instance if no cached copy is found. When encountering too many patterns to cache at 355 | * runtime (the threshold is 65536), it turns the default cache off, assuming that arbitrary permutations of 356 | * patterns are coming in, with little chance for encountering a reoccurring pattern. 357 | *

358 | * This method may get overridden to implement a custom cache strategy. 359 | * 360 | * @param pattern 361 | * the pattern to match against (never {@code null}) 362 | * @return a corresponding AntPathStringMatcher (never {@code null}) 363 | * @see #setCachePatterns 364 | */ 365 | protected AntPathStringMatcher getStringMatcher(String pattern) { 366 | AntPathStringMatcher matcher = null; 367 | Boolean cachePatterns = this.cachePatterns; 368 | if (cachePatterns == null || cachePatterns.booleanValue()) { 369 | matcher = this.stringMatcherCache.get(pattern); 370 | } 371 | if (matcher == null) { 372 | matcher = new AntPathStringMatcher(pattern); 373 | if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { 374 | // Try to adapt to the runtime situation that we're encountering: 375 | // There are obviously too many different patterns coming in here... 376 | // So let's turn off the cache since the patterns are unlikely to be reoccurring. 377 | deactivatePatternCache(); 378 | return matcher; 379 | } 380 | if (cachePatterns == null || cachePatterns.booleanValue()) { 381 | this.stringMatcherCache.put(pattern, matcher); 382 | } 383 | } 384 | return matcher; 385 | } 386 | 387 | /** 388 | * Given a pattern and a full path, determine the pattern-mapped part. 389 | *

390 | * For example: 391 | *

    392 | *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
  • 393 | *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • 394 | *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
  • 395 | *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • 396 | *
  • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
  • 397 | *
  • '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
  • 398 | *
  • '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
  • 399 | *
  • '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
  • 400 | *
401 | *

402 | * Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but does 403 | * not enforce this. 404 | */ 405 | public String extractPathWithinPattern(String pattern, String path) { 406 | String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); 407 | String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 408 | StringBuilder builder = new StringBuilder(); 409 | boolean pathStarted = false; 410 | 411 | for (int segment = 0; segment < patternParts.length; segment++) { 412 | String patternPart = patternParts[segment]; 413 | if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { 414 | for (; segment < pathParts.length; segment++) { 415 | if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { 416 | builder.append(this.pathSeparator); 417 | } 418 | builder.append(pathParts[segment]); 419 | pathStarted = true; 420 | } 421 | } 422 | } 423 | 424 | return builder.toString(); 425 | } 426 | 427 | public Map extractUriTemplateVariables(String pattern, String path) { 428 | Map variables = new LinkedHashMap(); 429 | boolean result = doMatch(pattern, path, true, variables); 430 | if (!result) { 431 | throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); 432 | } 433 | return variables; 434 | } 435 | 436 | /** 437 | * Combines two patterns into a new pattern that is returned. 438 | *

439 | * This implementation simply concatenates the two patterns, unless the first pattern contains a file extension 440 | * match (such as {@code *.html}. In that case, the second pattern should be included in the first, or an 441 | * {@code IllegalArgumentException} is thrown. 442 | *

443 | * For example: 444 | * 445 | * 446 | * 447 | * 448 | * 449 | * 450 | * 451 | * 452 | * 453 | * 454 | * 455 | * 456 | * 457 | * 458 | * 459 | * 460 | * 461 | * 462 | * 463 | * 464 | * 465 | * 466 | * 467 | * 468 | * 469 | * 470 | * 471 | * 472 | * 473 | * 474 | * 475 | * 476 | * 477 | * 478 | * 479 | * 480 | * 481 | * 482 | * 483 | * 484 | * 485 | * 486 | * 487 | * 488 | * 489 | * 490 | * 491 | * 492 | * 493 | * 494 | * 495 | * 496 | * 497 | * 498 | * 499 | * 500 | * 501 | * 502 | * 503 | * 504 | * 505 | * 506 | * 507 | * 508 | * 509 | * 510 | *
Pattern 1Pattern 2Result
/hotels{@code null}/hotels
{@code null}/hotels/hotels
/hotels/bookings/hotels/bookings
/hotelsbookings/hotels/bookings
/hotels/*/bookings/hotels/bookings
/hotels/**/bookings/hotels/**/bookings
/hotels{hotel}/hotels/{hotel}
/hotels/*{hotel}/hotels/{hotel}
/hotels/**{hotel}/hotels/**/{hotel}
/*.html/hotels.html/hotels.html
/*.html/hotels/hotels.html
/*.html/*.txtIllegalArgumentException
511 | * 512 | * @param pattern1 513 | * the first pattern 514 | * @param pattern2 515 | * the second pattern 516 | * @return the combination of the two patterns 517 | * @throws IllegalArgumentException 518 | * when the two patterns cannot be combined 519 | */ 520 | public String combine(String pattern1, String pattern2) { 521 | if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) { 522 | return ""; 523 | } 524 | if (!StringUtils.hasText(pattern1)) { 525 | return pattern2; 526 | } 527 | if (!StringUtils.hasText(pattern2)) { 528 | return pattern1; 529 | } 530 | 531 | boolean pattern1ContainsUriVar = pattern1.indexOf('{') != -1; 532 | if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { 533 | // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html 534 | // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar 535 | return pattern2; 536 | } 537 | 538 | // /hotels/* + /booking -> /hotels/booking 539 | // /hotels/* + booking -> /hotels/booking 540 | if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { 541 | return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); 542 | } 543 | 544 | // /hotels/** + /booking -> /hotels/**/booking 545 | // /hotels/** + booking -> /hotels/**/booking 546 | if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { 547 | return concat(pattern1, pattern2); 548 | } 549 | 550 | int starDotPos1 = pattern1.indexOf("*."); 551 | if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { 552 | // simply concatenate the two patterns 553 | return concat(pattern1, pattern2); 554 | } 555 | String extension1 = pattern1.substring(starDotPos1 + 1); 556 | int dotPos2 = pattern2.indexOf('.'); 557 | String fileName2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); 558 | String extension2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); 559 | String extension = extension1.startsWith("*") ? extension2 : extension1; 560 | return fileName2 + extension; 561 | } 562 | 563 | private String concat(String path1, String path2) { 564 | if (path1.endsWith(this.pathSeparator) || path2.startsWith(this.pathSeparator)) { 565 | return path1 + path2; 566 | } 567 | return path1 + this.pathSeparator + path2; 568 | } 569 | 570 | /** 571 | * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of explicitness. 572 | *

573 | * The returned {@code Comparator} will 574 | * {@linkplain java.util.Collections#sort(java.util.List, java.util.Comparator) sort} a list so that more specific 575 | * patterns (without uri templates or wild cards) come before generic patterns. So given a list with the following 576 | * patterns: 577 | *

    578 | *
  1. {@code /hotels/new}
  2. 579 | *
  3. {@code /hotels/ hotel}
  4. 580 | *
  5. {@code /hotels/*}
  6. 581 | *
582 | * the returned comparator will sort this list so that the order will be as indicated. 583 | *

584 | * The full path given as parameter is used to test for exact matches. So when the given path is {@code /hotels/2}, 585 | * the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. 586 | * 587 | * @param path 588 | * the full path to use for comparison 589 | * @return a comparator capable of sorting patterns in order of explicitness 590 | */ 591 | public Comparator getPatternComparator(String path) { 592 | return new AntPatternComparator(path); 593 | } 594 | 595 | /** 596 | * Tests whether or not a string matches against a pattern via a {@link Pattern}. 597 | *

598 | * The pattern may contain special characters: '*' means zero or more characters; '?' means one and only one 599 | * character; '{' and '}' indicate a URI template pattern. For example /users/{user}. 600 | */ 601 | protected static class AntPathStringMatcher { 602 | 603 | private static final Pattern GLOB_PATTERN = Pattern 604 | .compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); 605 | 606 | private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; 607 | 608 | private final Pattern pattern; 609 | 610 | private final List variableNames = new LinkedList(); 611 | 612 | public AntPathStringMatcher(String pattern) { 613 | StringBuilder patternBuilder = new StringBuilder(); 614 | Matcher m = GLOB_PATTERN.matcher(pattern); 615 | int end = 0; 616 | while (m.find()) { 617 | patternBuilder.append(quote(pattern, end, m.start())); 618 | String match = m.group(); 619 | if ("?".equals(match)) { 620 | patternBuilder.append('.'); 621 | } else if ("*".equals(match)) { 622 | patternBuilder.append(".*"); 623 | } else if (match.startsWith("{") && match.endsWith("}")) { 624 | int colonIdx = match.indexOf(':'); 625 | if (colonIdx == -1) { 626 | patternBuilder.append(DEFAULT_VARIABLE_PATTERN); 627 | this.variableNames.add(m.group(1)); 628 | } else { 629 | String variablePattern = match.substring(colonIdx + 1, match.length() - 1); 630 | patternBuilder.append('('); 631 | patternBuilder.append(variablePattern); 632 | patternBuilder.append(')'); 633 | String variableName = match.substring(1, colonIdx); 634 | this.variableNames.add(variableName); 635 | } 636 | } 637 | end = m.end(); 638 | } 639 | patternBuilder.append(quote(pattern, end, pattern.length())); 640 | this.pattern = Pattern.compile(patternBuilder.toString()); 641 | } 642 | 643 | private String quote(String s, int start, int end) { 644 | if (start == end) { 645 | return ""; 646 | } 647 | return Pattern.quote(s.substring(start, end)); 648 | } 649 | 650 | /** 651 | * Main entry point. 652 | * 653 | * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. 654 | */ 655 | public boolean matchStrings(String str, Map uriTemplateVariables) { 656 | Matcher matcher = this.pattern.matcher(str); 657 | if (matcher.matches()) { 658 | if (uriTemplateVariables != null) { 659 | // SPR-8455 660 | if (this.variableNames.size() != matcher.groupCount()) { 661 | throw new IllegalStateException( 662 | "The number of capturing groups in the pattern segment " 663 | + this.pattern 664 | + " does not match the number of URI template variables it defines, which can occur if " 665 | + " capturing groups are used in a URI template regex. Use non-capturing groups instead."); 666 | } 667 | for (int i = 1; i <= matcher.groupCount(); i++) { 668 | String name = this.variableNames.get(i - 1); 669 | String value = matcher.group(i); 670 | uriTemplateVariables.put(name, value); 671 | } 672 | } 673 | return true; 674 | } else { 675 | return false; 676 | } 677 | } 678 | } 679 | 680 | /** 681 | * The default {@link Comparator} implementation returned by {@link #getPatternComparator(String)}. 682 | *

683 | * In order, the most "generic" pattern is determined by the following: 684 | *

    685 | *
  • if it's null or a capture all pattern (i.e. it is equal to "/**")
  • 686 | *
  • if the other pattern is an actual match
  • 687 | *
  • if it's a catch-all pattern (i.e. it ends with "**"
  • 688 | *
  • if it's got more "*" than the other pattern
  • 689 | *
  • if it's got more "{foo}" than the other pattern
  • 690 | *
  • if it's shorter than the other pattern
  • 691 | *
692 | */ 693 | protected static class AntPatternComparator implements Comparator { 694 | 695 | private final String path; 696 | 697 | public AntPatternComparator(String path) { 698 | this.path = path; 699 | } 700 | 701 | /** 702 | * Compare two patterns to determine which should match first, i.e. which 703 | * is the most specific regarding the current path. 704 | * 705 | * @return a negative integer, zero, or a positive integer as pattern1 is 706 | * more specific, equally specific, or less specific than pattern2. 707 | */ 708 | public int compare(String pattern1, String pattern2) { 709 | PatternInfo info1 = new PatternInfo(pattern1); 710 | PatternInfo info2 = new PatternInfo(pattern2); 711 | 712 | if (info1.isLeastSpecific() && info2.isLeastSpecific()) { 713 | return 0; 714 | } else if (info1.isLeastSpecific()) { 715 | return 1; 716 | } else if (info2.isLeastSpecific()) { 717 | return -1; 718 | } 719 | 720 | boolean pattern1EqualsPath = pattern1.equals(path); 721 | boolean pattern2EqualsPath = pattern2.equals(path); 722 | if (pattern1EqualsPath && pattern2EqualsPath) { 723 | return 0; 724 | } else if (pattern1EqualsPath) { 725 | return -1; 726 | } else if (pattern2EqualsPath) { 727 | return 1; 728 | } 729 | 730 | if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { 731 | return 1; 732 | } else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { 733 | return -1; 734 | } 735 | 736 | if (info1.getTotalCount() != info2.getTotalCount()) { 737 | return info1.getTotalCount() - info2.getTotalCount(); 738 | } 739 | 740 | if (info1.getLength() != info2.getLength()) { 741 | return info2.getLength() - info1.getLength(); 742 | } 743 | 744 | if (info1.getSingleWildcards() < info2.getSingleWildcards()) { 745 | return -1; 746 | } else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { 747 | return 1; 748 | } 749 | 750 | if (info1.getUriVars() < info2.getUriVars()) { 751 | return -1; 752 | } else if (info2.getUriVars() < info1.getUriVars()) { 753 | return 1; 754 | } 755 | 756 | return 0; 757 | } 758 | 759 | /** 760 | * Value class that holds information about the pattern, e.g. number of 761 | * occurrences of "*", "**", and "{" pattern elements. 762 | */ 763 | private static class PatternInfo { 764 | 765 | private final String pattern; 766 | 767 | private int uriVars; 768 | 769 | private int singleWildcards; 770 | 771 | private int doubleWildcards; 772 | 773 | private boolean catchAllPattern; 774 | 775 | private boolean prefixPattern; 776 | 777 | private Integer length; 778 | 779 | public PatternInfo(String pattern) { 780 | this.pattern = pattern; 781 | if (this.pattern != null) { 782 | initCounters(); 783 | this.catchAllPattern = this.pattern.equals("/**"); 784 | this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); 785 | } 786 | if (this.uriVars == 0) { 787 | this.length = (this.pattern != null ? this.pattern.length() : 0); 788 | } 789 | } 790 | 791 | protected void initCounters() { 792 | int pos = 0; 793 | while (pos < this.pattern.length()) { 794 | if (this.pattern.charAt(pos) == '{') { 795 | this.uriVars++; 796 | pos++; 797 | } else if (this.pattern.charAt(pos) == '*') { 798 | if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { 799 | this.doubleWildcards++; 800 | pos += 2; 801 | } else if (!this.pattern.substring(pos - 1).equals(".*")) { 802 | this.singleWildcards++; 803 | pos++; 804 | } else { 805 | pos++; 806 | } 807 | } else { 808 | pos++; 809 | } 810 | } 811 | } 812 | 813 | public int getUriVars() { 814 | return this.uriVars; 815 | } 816 | 817 | public int getSingleWildcards() { 818 | return this.singleWildcards; 819 | } 820 | 821 | public int getDoubleWildcards() { 822 | return this.doubleWildcards; 823 | } 824 | 825 | public boolean isLeastSpecific() { 826 | return (this.pattern == null || this.catchAllPattern); 827 | } 828 | 829 | public boolean isPrefixPattern() { 830 | return this.prefixPattern; 831 | } 832 | 833 | public int getTotalCount() { 834 | return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); 835 | } 836 | 837 | /** 838 | * Returns the length of the given pattern, where template variables are considered to be 1 long. 839 | */ 840 | public int getLength() { 841 | if (this.length == null) { 842 | this.length = VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length(); 843 | } 844 | return this.length; 845 | } 846 | } 847 | } 848 | 849 | /** 850 | * A simple cache for patterns that depend on the configured path separator. 851 | */ 852 | private static class PathSeparatorPatternCache { 853 | 854 | private final String endsOnWildCard; 855 | 856 | private final String endsOnDoubleWildCard; 857 | 858 | public PathSeparatorPatternCache(String pathSeparator) { 859 | this.endsOnWildCard = pathSeparator + "*"; 860 | this.endsOnDoubleWildCard = pathSeparator + "**"; 861 | } 862 | 863 | public String getEndsOnWildCard() { 864 | return this.endsOnWildCard; 865 | } 866 | 867 | public String getEndsOnDoubleWildCard() { 868 | return this.endsOnDoubleWildCard; 869 | } 870 | } 871 | 872 | } 873 | --------------------------------------------------------------------------------