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