1 /*
2 * Copyright 2014-2020 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 * https://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 package org.springframework.data.mapping.context;
17
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.Iterator;
21 import java.util.Optional;
22 import java.util.function.BiFunction;
23 import java.util.stream.Collectors;
24
25 import org.springframework.data.mapping.PersistentEntity;
26 import org.springframework.data.mapping.PersistentProperty;
27 import org.springframework.data.util.Streamable;
28 import org.springframework.data.util.TypeInformation;
29 import org.springframework.lang.Nullable;
30 import org.springframework.util.Assert;
31
32 /**
33 * Value object to access {@link PersistentEntity} instances managed by {@link MappingContext}s.
34 *
35 * @author Oliver Gierke
36 * @since 1.8
37 */
38 public class PersistentEntities implements Streamable<PersistentEntity<?, ? extends PersistentProperty<?>>> {
39
40 private final Streamable<? extends MappingContext<?, ? extends PersistentProperty<?>>> contexts;
41
42 /**
43 * Creates a new {@link PersistentEntities} for the given {@link MappingContext}s.
44 *
45 * @param contexts
46 */
47 public PersistentEntities(Iterable<? extends MappingContext<?, ?>> contexts) {
48
49 Assert.notNull(contexts, "MappingContexts must not be null!");
50
51 this.contexts = Streamable.of(contexts);
52 }
53
54 /**
55 * Creates a new {@link PersistentEntities} for the given {@link MappingContext}s.
56 *
57 * @param contexts must not be {@literal null}.
58 * @return
59 */
60 public static PersistentEntities of(MappingContext<?, ?>... contexts) {
61
62 Assert.notNull(contexts, "MappingContexts must not be null!");
63
64 return new PersistentEntities(Arrays.asList(contexts));
65 }
66
67 /**
68 * Returns the {@link PersistentEntity} for the given type. Will consider all {@link MappingContext}s registered but
69 * return {@literal Optional#empty()} in case none of the registered ones already have a {@link PersistentEntity}
70 * registered for the given type.
71 *
72 * @param type can be {@literal null}.
73 * @return
74 */
75 public Optional<PersistentEntity<?, ? extends PersistentProperty<?>>> getPersistentEntity(Class<?> type) {
76
77 return contexts.stream()//
78 .filter(it -> it.hasPersistentEntityFor(type))//
79 .findFirst().map(it -> it.getRequiredPersistentEntity(type));
80 }
81
82 /**
83 * Returns the {@link PersistentEntity} for the given type. Will consider all {@link MappingContext}s registered but
84 * throw an {@link IllegalArgumentException} in case none of the registered ones already have a
85 * {@link PersistentEntity} registered for the given type.
86 *
87 * @param type must not be {@literal null}.
88 * @return the {@link PersistentEntity} for the given domain type.
89 * @throws IllegalArgumentException in case no {@link PersistentEntity} can be found for the given type.
90 */
91 public PersistentEntity<?, ? extends PersistentProperty<?>> getRequiredPersistentEntity(Class<?> type) {
92
93 Assert.notNull(type, "Domain type must not be null!");
94
95 return getPersistentEntity(type).orElseThrow(
96 () -> new IllegalArgumentException(String.format("Couldn't find PersistentEntity for type %s!", type)));
97 }
98
99 /**
100 * Executes the given {@link BiFunction} on the given {@link MappingContext} and {@link PersistentEntity} based on the
101 * given type.
102 *
103 * @param type must not be {@literal null}.
104 * @param combiner must not be {@literal null}.
105 * @return
106 */
107 public <T> Optional<T> mapOnContext(Class<?> type,
108 BiFunction<MappingContext<?, ? extends PersistentProperty<?>>, PersistentEntity<?, ?>, T> combiner) {
109
110 Assert.notNull(type, "Type must not be null!");
111 Assert.notNull(combiner, "Combining BiFunction must not be null!");
112
113 return contexts.stream() //
114 .filter(it -> it.hasPersistentEntityFor(type)) //
115 .map(it -> combiner.apply(it, it.getRequiredPersistentEntity(type))) //
116 .findFirst();
117 }
118
119 /**
120 * Returns all {@link TypeInformation} exposed by the registered {@link MappingContext}s.
121 *
122 * @return
123 */
124 public Streamable<TypeInformation<?>> getManagedTypes() {
125
126 return Streamable.of(contexts.stream()//
127 .flatMap(it -> it.getManagedTypes().stream())//
128 .collect(Collectors.toSet()));
129 }
130
131 /*
132 * (non-Javadoc)
133 * @see java.lang.Iterable#iterator()
134 */
135 @Override
136 public Iterator<PersistentEntity<?, ? extends PersistentProperty<?>>> iterator() {
137
138 return contexts.stream()
139 .<PersistentEntity<?, ? extends PersistentProperty<?>>> flatMap(it -> it.getPersistentEntities().stream())
140 .collect(Collectors.toList()).iterator();
141 }
142
143 /**
144 * Returns the {@link PersistentEntity} the given {@link PersistentProperty} refers to in case it's an association.
145 * For direct aggregate references, that's simply the entity for the {@link PersistentProperty}'s actual type. If the
146 * property type is not an entity - as it might rather refer to the identifier type - we either use the reference's
147 * defined target type and fall back to trying to find a {@link PersistentEntity} identified by the
148 * {@link PersistentProperty}'s actual type.
149 *
150 * @param property must not be {@literal null}.
151 * @return
152 * @since 2.1
153 */
154 @Nullable
155 public PersistentEntity<?, ?> getEntityUltimatelyReferredToBy(PersistentProperty<?> property) {
156
157 TypeInformation<?> propertyType = property.getTypeInformation().getActualType();
158
159 if (propertyType == null || !property.isAssociation()) {
160 return null;
161 }
162
163 Class<?> associationTargetType = property.getAssociationTargetType();
164
165 return associationTargetType == null //
166 ? getEntityIdentifiedBy(propertyType) //
167 : getPersistentEntity(associationTargetType).orElseGet(() -> getEntityIdentifiedBy(propertyType));
168 }
169
170 /**
171 * Returns the type the given {@link PersistentProperty} ultimately refers to. In case it's of a unique identifier
172 * type of an entity known it'll return the entity type.
173 *
174 * @param property must not be {@literal null}.
175 * @return
176 */
177 public TypeInformation<?> getTypeUltimatelyReferredToBy(PersistentProperty<?> property) {
178
179 Assert.notNull(property, "PersistentProperty must not be null!");
180
181 PersistentEntity<?, ?> entity = getEntityUltimatelyReferredToBy(property);
182
183 return entity == null //
184 ? property.getTypeInformation().getRequiredActualType() //
185 : entity.getTypeInformation();
186 }
187
188 /**
189 * Returns the {@link PersistentEntity} identified by the given type.
190 *
191 * @param type
192 * @return
193 * @throws IllegalStateException if the entity cannot be detected uniquely as multiple ones might share the same
194 * identifier.
195 */
196 @Nullable
197 private PersistentEntity<?, ?> getEntityIdentifiedBy(TypeInformation<?> type) {
198
199 Collection<PersistentEntity<?, ?>> entities = contexts.stream() //
200 .flatMap(it -> it.getPersistentEntities().stream()) //
201 .map(it -> it.getIdProperty()) //
202 .filter(it -> it != null && type.equals(it.getTypeInformation().getActualType())) //
203 .map(it -> it.getOwner()) //
204 .collect(Collectors.toList());
205
206 if (entities.size() > 1) {
207
208 String message = "Found multiple entities identified by " + type.getType() + ": ";
209 message += entities.stream().map(it -> it.getType().getName()).collect(Collectors.joining(", "));
210 message += "! Introduce dedciated unique identifier types or explicitly define the target type in @Reference!";
211
212 throw new IllegalStateException(message);
213 }
214
215 return entities.isEmpty() ? null : entities.iterator().next();
216 }
217 }
218