|
15 | 15 | */ |
16 | 16 | package org.springframework.data.mongodb.core.convert; |
17 | 17 |
|
18 | | -import java.util.HashMap; |
19 | 18 | import java.util.LinkedHashMap; |
20 | | -import java.util.Locale; |
21 | 19 | import java.util.Map; |
22 | 20 | import java.util.Map.Entry; |
| 21 | +import java.util.WeakHashMap; |
23 | 22 | import java.util.regex.Matcher; |
24 | 23 | import java.util.regex.Pattern; |
25 | 24 |
|
26 | 25 | import org.bson.Document; |
27 | | - |
28 | 26 | import org.springframework.core.convert.ConversionService; |
| 27 | +import org.springframework.dao.InvalidDataAccessApiUsageException; |
| 28 | +import org.springframework.data.annotation.Reference; |
29 | 29 | import org.springframework.data.mapping.PersistentPropertyAccessor; |
| 30 | +import org.springframework.data.mapping.PersistentPropertyPath; |
| 31 | +import org.springframework.data.mapping.PropertyPath; |
30 | 32 | import org.springframework.data.mapping.context.MappingContext; |
31 | 33 | import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; |
32 | 34 | import org.springframework.data.mongodb.core.mapping.DocumentPointer; |
33 | 35 | import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; |
34 | 36 | import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; |
35 | 37 |
|
36 | 38 | /** |
| 39 | + * Internal API to construct {@link DocumentPointer} for a given property. Considers {@link LazyLoadingProxy}, |
| 40 | + * registered {@link Object} to {@link DocumentPointer} {@link org.springframework.core.convert.converter.Converter}, |
| 41 | + * simple {@literal _id} lookups and cases where the {@link DocumentPointer} needs to be computed via a lookup query. |
| 42 | + * |
37 | 43 | * @author Christoph Strobl |
38 | 44 | * @since 3.3 |
39 | 45 | */ |
40 | 46 | class DocumentPointerFactory { |
41 | 47 |
|
42 | 48 | private final ConversionService conversionService; |
43 | 49 | private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext; |
44 | | - private final Map<String, LinkageDocument> linkageMap; |
45 | | - |
46 | | - public DocumentPointerFactory(ConversionService conversionService, |
| 50 | + private final Map<String, LinkageDocument> cache; |
| 51 | + |
| 52 | + /** |
| 53 | + * A {@link Pattern} matching quoted and unquoted variants (with/out whitespaces) of |
| 54 | + * <code>{'_id' : ?#{#target} }</code>. |
| 55 | + */ |
| 56 | + private static final Pattern DEFAULT_LOOKUP_PATTERN = Pattern.compile("\\{\\s?" + // document start (whitespace opt) |
| 57 | + "['\"]?_id['\"]?" + // followed by an optionally quoted _id. Like: _id, '_id' or "_id" |
| 58 | + "?\\s?:\\s?" + // then a colon optionally wrapped inside whitespaces |
| 59 | + "['\"]?\\?#\\{#target\\}['\"]?" + // leading to the potentially quoted ?#{#target} expression |
| 60 | + "\\s*}"); // some optional whitespaces and document close |
| 61 | + |
| 62 | + DocumentPointerFactory(ConversionService conversionService, |
47 | 63 | MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) { |
48 | 64 |
|
49 | 65 | this.conversionService = conversionService; |
50 | 66 | this.mappingContext = mappingContext; |
51 | | - this.linkageMap = new HashMap<>(); |
| 67 | + this.cache = new WeakHashMap<>(); |
52 | 68 | } |
53 | 69 |
|
54 | | - public DocumentPointer<?> computePointer(MongoPersistentProperty property, Object value, Class<?> typeHint) { |
| 70 | + DocumentPointer<?> computePointer( |
| 71 | + MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, |
| 72 | + MongoPersistentProperty property, Object value, Class<?> typeHint) { |
55 | 73 |
|
56 | 74 | if (value instanceof LazyLoadingProxy) { |
57 | 75 | return () -> ((LazyLoadingProxy) value).getSource(); |
58 | 76 | } |
59 | 77 |
|
60 | 78 | if (conversionService.canConvert(typeHint, DocumentPointer.class)) { |
61 | 79 | return conversionService.convert(value, DocumentPointer.class); |
62 | | - } else { |
| 80 | + } |
63 | 81 |
|
64 | | - MongoPersistentEntity<?> persistentEntity = mappingContext |
65 | | - .getRequiredPersistentEntity(property.getAssociationTargetType()); |
| 82 | + MongoPersistentEntity<?> persistentEntity = mappingContext |
| 83 | + .getRequiredPersistentEntity(property.getAssociationTargetType()); |
66 | 84 |
|
67 | | - // TODO: Extract method |
68 | | - if (!property.getDocumentReference().lookup().toLowerCase(Locale.ROOT).replaceAll("\\s", "").replaceAll("'", "") |
69 | | - .equals("{_id:?#{#target}}")) { |
| 85 | + if (usesDefaultLookup(property)) { |
| 86 | + return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier(); |
| 87 | + } |
70 | 88 |
|
71 | | - MongoPersistentEntity<?> valueEntity = mappingContext.getPersistentEntity(value.getClass()); |
72 | | - PersistentPropertyAccessor<Object> propertyAccessor; |
73 | | - if (valueEntity == null) { |
74 | | - propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), |
75 | | - value); |
76 | | - } else { |
77 | | - propertyAccessor = valueEntity.getPropertyAccessor(value); |
| 89 | + MongoPersistentEntity<?> valueEntity = mappingContext.getPersistentEntity(value.getClass()); |
| 90 | + PersistentPropertyAccessor<Object> propertyAccessor; |
| 91 | + if (valueEntity == null) { |
| 92 | + propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value); |
| 93 | + } else { |
| 94 | + propertyAccessor = valueEntity.getPropertyPathAccessor(value); |
| 95 | + } |
78 | 96 |
|
79 | | - } |
| 97 | + return cache.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::from) |
| 98 | + .getDocumentPointer(mappingContext, persistentEntity, propertyAccessor); |
| 99 | + } |
80 | 100 |
|
81 | | - return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::new) |
82 | | - .get(persistentEntity, propertyAccessor); |
83 | | - } |
| 101 | + private boolean usesDefaultLookup(MongoPersistentProperty property) { |
84 | 102 |
|
85 | | - // just take the id as a reference |
86 | | - return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier(); |
| 103 | + if (property.isDocumentReference()) { |
| 104 | + return DEFAULT_LOOKUP_PATTERN.matcher(property.getDocumentReference().lookup()).matches(); |
| 105 | + } |
| 106 | + |
| 107 | + Reference atReference = property.findAnnotation(Reference.class); |
| 108 | + if (atReference != null) { |
| 109 | + return true; |
87 | 110 | } |
| 111 | + |
| 112 | + throw new IllegalStateException(String.format("%s does not seem to be define Reference", property)); |
88 | 113 | } |
89 | 114 |
|
| 115 | + /** |
| 116 | + * Value object that computes a document pointer from a given lookup query by identifying SpEL expressions and |
| 117 | + * inverting it. |
| 118 | + * |
| 119 | + * <pre class="code"> |
| 120 | + * // source |
| 121 | + * { 'firstname' : ?#{fn}, 'lastname' : '?#{ln} } |
| 122 | + * |
| 123 | + * // target |
| 124 | + * { 'fn' : ..., 'ln' : ... } |
| 125 | + * </pre> |
| 126 | + * |
| 127 | + * The actual pointer is the computed via |
| 128 | + * {@link #getDocumentPointer(MappingContext, MongoPersistentEntity, PersistentPropertyAccessor)} applying values from |
| 129 | + * the provided {@link PersistentPropertyAccessor} to the target document by looking at the keys of the expressions |
| 130 | + * from the source. |
| 131 | + */ |
90 | 132 | static class LinkageDocument { |
91 | 133 |
|
92 | | - static final Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}"); |
| 134 | + static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\?#\\{#?(?<fieldName>[\\w\\d\\.\\-)]*)\\}"); |
| 135 | + static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("###_(?<index>\\d*)_###"); |
93 | 136 |
|
94 | | - String lookup; |
95 | | - org.bson.Document fetchDocument; |
96 | | - Map<Integer, String> mapMap; |
| 137 | + private final String lookup; |
| 138 | + private final org.bson.Document documentPointer; |
| 139 | + private final Map<String, String> placeholderMap; |
97 | 140 |
|
98 | | - public LinkageDocument(String lookup) { |
| 141 | + static LinkageDocument from(String lookup) { |
| 142 | + return new LinkageDocument(lookup); |
| 143 | + } |
99 | 144 |
|
100 | | - this.lookup = lookup; |
101 | | - String targetLookup = lookup; |
| 145 | + private LinkageDocument(String lookup) { |
102 | 146 |
|
| 147 | + this.lookup = lookup; |
| 148 | + this.placeholderMap = new LinkedHashMap<>(); |
103 | 149 |
|
104 | | - Matcher matcher = pattern.matcher(lookup); |
105 | 150 | int index = 0; |
106 | | - mapMap = new LinkedHashMap<>(); |
| 151 | + Matcher matcher = EXPRESSION_PATTERN.matcher(lookup); |
| 152 | + String targetLookup = lookup; |
107 | 153 |
|
108 | | - // TODO: Make explicit what's happening here |
109 | 154 | while (matcher.find()) { |
110 | 155 |
|
111 | | - String expr = matcher.group(); |
112 | | - String sanitized = expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "") |
113 | | - .replace("target.", "").replaceAll("'", ""); |
114 | | - mapMap.put(index, sanitized); |
115 | | - targetLookup = targetLookup.replace(expr, index + ""); |
| 156 | + String expression = matcher.group(); |
| 157 | + String fieldName = matcher.group("fieldName").replace("target.", ""); |
| 158 | + |
| 159 | + String placeholder = placeholder(index); |
| 160 | + placeholderMap.put(placeholder, fieldName); |
| 161 | + targetLookup = targetLookup.replace(expression, "'" + placeholder + "'"); |
116 | 162 | index++; |
117 | 163 | } |
118 | 164 |
|
119 | | - fetchDocument = org.bson.Document.parse(targetLookup); |
| 165 | + this.documentPointer = org.bson.Document.parse(targetLookup); |
120 | 166 | } |
121 | 167 |
|
122 | | - org.bson.Document get(MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) { |
| 168 | + private String placeholder(int index) { |
| 169 | + return "###_" + index + "_###"; |
| 170 | + } |
123 | 171 |
|
124 | | - org.bson.Document targetDocument = new Document(); |
| 172 | + private boolean isPlaceholder(String key) { |
| 173 | + return PLACEHOLDER_PATTERN.matcher(key).matches(); |
| 174 | + } |
125 | 175 |
|
126 | | - // TODO: recursive matching over nested Documents or would the parameter binding json parser be a thing? |
127 | | - // like we have it ordered by index values and could provide the parameter array from it. |
| 176 | + DocumentPointer<Object> getDocumentPointer( |
| 177 | + MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, |
| 178 | + MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) { |
| 179 | + return () -> updatePlaceholders(documentPointer, new Document(), mappingContext, persistentEntity, |
| 180 | + propertyAccessor); |
| 181 | + } |
| 182 | + |
| 183 | + Document updatePlaceholders(org.bson.Document source, org.bson.Document target, |
| 184 | + MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, |
| 185 | + MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) { |
128 | 186 |
|
129 | | - for (Entry<String, Object> entry : fetchDocument.entrySet()) { |
| 187 | + for (Entry<String, Object> entry : source.entrySet()) { |
| 188 | + |
| 189 | + if (entry.getKey().startsWith("$")) { |
| 190 | + throw new InvalidDataAccessApiUsageException(String.format( |
| 191 | + "Cannot derive document pointer from lookup '%s' using query operator (%s). Please consider registering a custom converter.", |
| 192 | + lookup, entry.getKey())); |
| 193 | + } |
130 | 194 |
|
131 | | - if (entry.getKey().equals("target")) { |
| 195 | + if (entry.getValue() instanceof Document) { |
132 | 196 |
|
133 | | - String refKey = mapMap.get(entry.getValue()); |
| 197 | + MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey()); |
| 198 | + if (persistentProperty != null && persistentProperty.isEntity()) { |
134 | 199 |
|
135 | | - if (persistentEntity.hasIdProperty()) { |
136 | | - targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty())); |
| 200 | + MongoPersistentEntity<?> nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType()); |
| 201 | + target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext, |
| 202 | + nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty)))); |
137 | 203 | } else { |
138 | | - targetDocument.put(refKey, propertyAccessor.getBean()); |
| 204 | + target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext, |
| 205 | + persistentEntity, propertyAccessor)); |
139 | 206 | } |
140 | 207 | continue; |
141 | 208 | } |
142 | 209 |
|
143 | | - Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey())); |
144 | | - String refKey = mapMap.get(entry.getValue()); |
145 | | - targetDocument.put(refKey, target); |
| 210 | + if (placeholderMap.containsKey(entry.getValue())) { |
| 211 | + |
| 212 | + String attribute = placeholderMap.get(entry.getValue()); |
| 213 | + if (attribute.contains(".")) { |
| 214 | + attribute = attribute.substring(attribute.lastIndexOf('.') + 1); |
| 215 | + } |
| 216 | + |
| 217 | + String fieldName = entry.getKey().equals("_id") ? "id" : entry.getKey(); |
| 218 | + if (!fieldName.contains(".")) { |
| 219 | + |
| 220 | + Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName)); |
| 221 | + target.put(attribute, targetValue); |
| 222 | + continue; |
| 223 | + } |
| 224 | + |
| 225 | + PersistentPropertyPath<?> path = mappingContext |
| 226 | + .getPersistentPropertyPath(PropertyPath.from(fieldName, persistentEntity.getTypeInformation())); |
| 227 | + Object targetValue = propertyAccessor.getProperty(path); |
| 228 | + target.put(attribute, targetValue); |
| 229 | + continue; |
| 230 | + } |
| 231 | + |
| 232 | + target.put(entry.getKey(), entry.getValue()); |
146 | 233 | } |
147 | | - return targetDocument; |
| 234 | + return target; |
148 | 235 | } |
149 | 236 | } |
150 | 237 | } |
0 commit comments