|
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