• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mybatis / mybatis-3 / #3889

19 Nov 2024 12:56PM UTC coverage: 87.254% (+0.07%) from 87.18%
#3889

Pull #3108

github

web-flow
Merge a01b27ed5 into 372319ae7
Pull Request #3108: 101: Add support for immutable collection constructor creation

3639 of 4419 branches covered (82.35%)

231 of 255 new or added lines in 5 files covered. (90.59%)

33 existing lines in 1 file now uncovered.

9577 of 10976 relevant lines covered (87.25%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.24
/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java
1
/*
2
 *    Copyright 2009-2024 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.apache.ibatis.executor.resultset;
17

18
import java.lang.reflect.Constructor;
19
import java.lang.reflect.ParameterizedType;
20
import java.lang.reflect.Type;
21
import java.util.ArrayList;
22
import java.util.Collection;
23
import java.util.Collections;
24
import java.util.HashMap;
25
import java.util.List;
26
import java.util.Map;
27
import java.util.Optional;
28
import java.util.stream.Collectors;
29

30
import org.apache.ibatis.executor.ExecutorException;
31
import org.apache.ibatis.mapping.ResultMap;
32
import org.apache.ibatis.mapping.ResultMapping;
33
import org.apache.ibatis.reflection.ReflectionException;
34
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
35
import org.apache.ibatis.reflection.factory.ObjectFactory;
36

37
/**
38
 * Represents an object that is still to be created once all nested results with collection values have been gathered
39
 *
40
 * @author Willie Scholtz
41
 */
42
final class PendingConstructorCreation {
43

44
  private final Class<?> resultType;
45
  private final List<Class<?>> constructorArgTypes;
46
  private final List<Object> constructorArgs;
47

48
  private final Map<Integer, PendingCreationMetaInfo> linkedCollectionMetaInfo;
49
  private final Map<PendingCreationKey, Collection<Object>> linkedCollectionsByKey;
50
  private final Map<PendingCreationKey, List<PendingConstructorCreation>> linkedCreationsByKey;
51

52
  PendingConstructorCreation(Class<?> resultType, List<Class<?>> types, List<Object> args) {
1✔
53
    // since all our keys are based on result map id, we know we will never go over args size
54
    final int maxSize = types.size();
1✔
55

56
    this.linkedCollectionMetaInfo = new HashMap<>(maxSize);
1✔
57
    this.linkedCollectionsByKey = new HashMap<>(maxSize);
1✔
58
    this.linkedCreationsByKey = new HashMap<>(maxSize);
1✔
59

60
    this.resultType = resultType;
1✔
61
    this.constructorArgTypes = types;
1✔
62
    this.constructorArgs = args;
1✔
63
  }
1✔
64

65
  @SuppressWarnings("unchecked")
66
  Collection<Object> initializeCollectionForResultMapping(ObjectFactory objectFactory, ResultMap resultMap,
67
      ResultMapping constructorMapping, Integer index) {
68
    final Class<?> parameterType = constructorMapping.getJavaType();
1✔
69
    if (!objectFactory.isCollection(parameterType)) {
1!
NEW
70
      throw new ReflectionException(
×
71
          "Cannot add a collection result to non-collection based resultMapping: " + constructorMapping);
72
    }
73

74
    final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping);
1✔
75
    return linkedCollectionsByKey.computeIfAbsent(creationKey, (k) -> {
1✔
76
      // this will allow us to verify the types of the collection before creating the final object
77
      linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), creationKey));
1✔
78

79
      // will be checked before we finally create the object) as we cannot reliably do that here
80
      return (Collection<Object>) objectFactory.create(parameterType);
1✔
81
    });
82
  }
83

84
  void linkCreation(ResultMapping constructorMapping, PendingConstructorCreation pcc) {
85
    final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping);
1✔
86
    final List<PendingConstructorCreation> pendingConstructorCreations = linkedCreationsByKey
1✔
87
        .computeIfAbsent(creationKey, (k) -> new ArrayList<>());
1✔
88

89
    if (pendingConstructorCreations.contains(pcc)) {
1!
NEW
90
      throw new ExecutorException("Cannot link inner constructor creation with same value, MyBatis internal error!");
×
91
    }
92

93
    pendingConstructorCreations.add(pcc);
1✔
94
  }
1✔
95

96
  void linkCollectionValue(ResultMapping constructorMapping, Object value) {
97
    // not necessary to add null results to the collection
98
    if (value == null) {
1!
NEW
99
      return;
×
100
    }
101

102
    final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping);
1✔
103
    if (!linkedCollectionsByKey.containsKey(creationKey)) {
1!
NEW
104
      throw new ExecutorException("Cannot link collection value for key: " + constructorMapping
×
105
          + ", resultMap has not been seen/initialized yet! Mybatis internal error!");
106
    }
107

108
    linkedCollectionsByKey.get(creationKey).add(value);
1✔
109
  }
1✔
110

111
  /**
112
   * Verifies preconditions before we can actually create the result object, this is more of a sanity check to ensure
113
   * all the mappings are as we expect them to be.
114
   * <p>
115
   * And if anything went wrong, provide the user with more information as to what went wrong
116
   *
117
   * @param objectFactory
118
   *          the object factory
119
   */
120
  private void verifyCanCreate(ObjectFactory objectFactory) {
121
    // if a custom object factory was supplied, we cannot reasionably verify that creation will work
122
    // thus, we disable verification and leave it up to the end user.
123
    if (!DefaultObjectFactory.class.equals(objectFactory.getClass())) {
1✔
124
      return;
1✔
125
    }
126

127
    // before we create, we need to get the constructor to be used and verify our types match
128
    // since we added to the collection completely unchecked
129
    final Constructor<?> resolvedConstructor = resolveConstructor(resultType, constructorArgTypes);
1✔
130
    final Type[] genericParameterTypes = resolvedConstructor.getGenericParameterTypes();
1✔
131
    for (int i = 0; i < genericParameterTypes.length; i++) {
1✔
132
      if (!linkedCollectionMetaInfo.containsKey(i)) {
1✔
133
        continue;
1✔
134
      }
135

136
      final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i);
1✔
137
      final Class<?> resolvedItemType = checkResolvedItemType(creationMetaInfo, genericParameterTypes[i]);
1✔
138

139
      // ensure we have an empty collection if there are linked creations for this arg
140
      final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey();
1✔
141
      if (linkedCreationsByKey.containsKey(pendingCreationKey)) {
1✔
142
        final Object emptyCollection = constructorArgs.get(i);
1✔
143
        if (emptyCollection == null || !objectFactory.isCollection(emptyCollection.getClass())) {
1!
NEW
144
          throw new ExecutorException(
×
145
              "Expected empty collection for '" + resolvedItemType + "', MyBatis internal error!");
146
        }
147
      } else {
1✔
148
        final Object linkedCollection = constructorArgs.get(i);
1✔
149
        if (!linkedCollectionsByKey.containsKey(pendingCreationKey)) {
1!
NEW
150
          throw new ExecutorException(
×
151
              "Expected linked collection for key '" + pendingCreationKey + "', not found! MyBatis internal error!");
152
        }
153

154
        // comparing memory locations here (we rely on that fact)
155
        if (linkedCollection != linkedCollectionsByKey.get(pendingCreationKey)) {
1!
NEW
156
          throw new ExecutorException("Expected linked collection in creation to be the same as arg for resultMap '"
×
157
              + pendingCreationKey + "', not equal! MyBatis internal error!");
158
        }
159
      }
160
    }
161
  }
1✔
162

163
  private static <T> Constructor<T> resolveConstructor(Class<T> type, List<Class<?>> constructorArgTypes) {
164
    try {
165
      if (constructorArgTypes == null) {
1!
NEW
166
        return type.getDeclaredConstructor();
×
167
      }
168

169
      return type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0]));
1✔
170
    } catch (Exception e) {
1✔
171
      String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList).stream()
1✔
172
          .map(Class::getSimpleName).collect(Collectors.joining(","));
1✔
173
      throw new ReflectionException(
1✔
174
          "Error resolving constructor for " + type + " with invalid types (" + argTypes + ") . Cause: " + e, e);
175
    }
176
  }
177

178
  private static Class<?> checkResolvedItemType(PendingCreationMetaInfo creationMetaInfo, Type genericParameterTypes) {
179
    final ParameterizedType genericParameterType = (ParameterizedType) genericParameterTypes;
1✔
180
    final Class<?> expectedType = (Class<?>) genericParameterType.getActualTypeArguments()[0];
1✔
181
    final Class<?> resolvedItemType = creationMetaInfo.getArgumentType();
1✔
182

183
    if (!expectedType.isAssignableFrom(resolvedItemType)) {
1!
NEW
184
      throw new ReflectionException(
×
185
          "Expected type '" + resolvedItemType + "', while the actual type of the collection was '" + expectedType
186
              + "', ensure your resultMap matches the type of the collection you are trying to inject");
187
    }
188

189
    return resolvedItemType;
1✔
190
  }
191

192
  @Override
193
  public String toString() {
NEW
194
    return "PendingConstructorCreation(" + this.hashCode() + "){" + "resultType=" + resultType + '}';
×
195
  }
196

197
  /**
198
   * Recursively creates the final result of this creation.
199
   *
200
   * @param objectFactory
201
   *          the object factory
202
   * @param verifyCreate
203
   *          should we verify this object can be created, should only be needed once
204
   *
205
   * @return the new immutable result
206
   */
207
  Object create(ObjectFactory objectFactory, boolean verifyCreate) {
208
    if (verifyCreate) {
1✔
209
      verifyCanCreate(objectFactory);
1✔
210
    }
211

212
    final List<Object> newArguments = new ArrayList<>(constructorArgs.size());
1✔
213
    for (int i = 0; i < constructorArgs.size(); i++) {
1✔
214
      final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i);
1✔
215
      final Object existingArg = constructorArgs.get(i);
1✔
216

217
      if (creationMetaInfo == null) {
1✔
218
        // we are not aware of this argument wrt pending creations
219
        newArguments.add(existingArg);
1✔
220
        continue;
1✔
221
      }
222

223
      // time to finally build this collection
224
      final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey();
1✔
225
      if (linkedCreationsByKey.containsKey(pendingCreationKey)) {
1✔
226
        @SuppressWarnings("unchecked")
227
        final Collection<Object> emptyCollection = (Collection<Object>) existingArg;
1✔
228
        final List<PendingConstructorCreation> linkedCreations = linkedCreationsByKey.get(pendingCreationKey);
1✔
229

230
        for (PendingConstructorCreation linkedCreation : linkedCreations) {
1✔
231
          emptyCollection.add(linkedCreation.create(objectFactory, verifyCreate));
1✔
232
        }
1✔
233

234
        newArguments.add(emptyCollection);
1✔
235
        continue;
1✔
236
      }
237

238
      // handle the base collection (it was built inline already)
239
      newArguments.add(existingArg);
1✔
240
    }
241

242
    return objectFactory.create(resultType, constructorArgTypes, newArguments);
1✔
243
  }
244
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc