From 17a99302f2f6276f745723f08fa83087f335b2c9 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Sat, 17 Dec 2022 12:39:38 +0900 Subject: [PATCH 001/133] Added a test case for #2754 Also related to #206 #575 --- .../bind_in_foreach/BindInForeachTest.java | 61 +++++++++++++++++++ .../submitted/bind_in_foreach/Mapper.java | 29 +++++++++ .../submitted/bind_in_foreach/User.java | 38 ++++++++++++ .../submitted/bind_in_foreach/CreateDB.sql | 22 +++++++ .../submitted/bind_in_foreach/Mapper.xml | 33 ++++++++++ .../bind_in_foreach/mybatis-config.xml | 41 +++++++++++++ 6 files changed, 224 insertions(+) create mode 100644 src/test/java/org/apache/ibatis/submitted/bind_in_foreach/BindInForeachTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/bind_in_foreach/Mapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/bind_in_foreach/User.java create mode 100644 src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/Mapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml diff --git a/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/BindInForeachTest.java b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/BindInForeachTest.java new file mode 100644 index 00000000000..ff6a3549de0 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/BindInForeachTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2009-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.bind_in_foreach; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.Reader; +import java.util.List; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class BindInForeachTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + try (Reader reader = Resources + .getResourceAsReader("org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml")) { + sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); + } + BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), + "org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql"); + } + + @Test + void testBindInForeach() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + assertEquals(3, mapper.createUsers(List.of(2, 4, 6))); + List users = mapper.selectUsers(); + assertEquals(3, users.size()); + assertEquals(1, users.get(0).getId()); + assertEquals("User2", users.get(0).getName()); + assertEquals(2, users.get(1).getId()); + assertEquals("User4", users.get(1).getName()); + assertEquals(3, users.get(2).getId()); + assertEquals("User6", users.get(2).getName()); + } + } + +} diff --git a/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/Mapper.java b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/Mapper.java new file mode 100644 index 00000000000..fd4d914a1c4 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/Mapper.java @@ -0,0 +1,29 @@ +/* + * Copyright 2009-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.bind_in_foreach; + +import java.util.List; + +import org.apache.ibatis.annotations.Select; + +public interface Mapper { + + @Select("select * from users order by id") + List selectUsers(); + + int createUsers(List numbers); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/User.java b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/User.java new file mode 100644 index 00000000000..765338fb82a --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/User.java @@ -0,0 +1,38 @@ +/* + * Copyright 2009-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.bind_in_foreach; + +public class User { + + private Integer id; + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql new file mode 100644 index 00000000000..23d3d13e50b --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql @@ -0,0 +1,22 @@ +-- +-- Copyright 2009-2022 the original author or authors. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +drop table users if exists; + +create table users ( + id int, + name varchar(20) +); diff --git a/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/Mapper.xml b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/Mapper.xml new file mode 100644 index 00000000000..c735771b349 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/Mapper.xml @@ -0,0 +1,33 @@ + + + + + + + insert into users (id, name) values + + + + (#{id}, #{name}) + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml new file mode 100644 index 00000000000..b694f3e402c --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + From 584990a347367c0d63c5b831cceee579c8568911 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Sat, 17 Dec 2022 12:07:35 +0900 Subject: [PATCH 002/133] When processing DynamicSqlSource, evaluate param values in scripting phase - Evaluated param values are stored in `ParameterMapping` and later used in DefaultParameterHandler - There is no change when processing RawSqlSource - Removed unused `injectionFilter` from TextSqlNode (gh-117) This should fix #2754 . This might also fix #206 and #575 , but with this patch, it still is not possible to invoke a method with parameter inside a parameter reference like `#{_parameter.mymethod(_parameter.value)}`. It might be possible to accept OGNL expression in a param reference (e.g. `#{${_parameter.mymethod(_parameter.value)}}`), but I'm not sure if that's a good idea. --- .../builder/ParameterMappingTokenHandler.java | 154 ++++++++++++++++++ .../ibatis/builder/SqlSourceBuilder.java | 118 +------------- .../apache/ibatis/executor/BaseExecutor.java | 4 +- .../ibatis/mapping/ParameterMapping.java | 17 ++ .../defaults/DefaultParameterHandler.java | 30 ++-- .../scripting/defaults/RawSqlSource.java | 26 +-- .../scripting/xmltags/DynamicContext.java | 63 ++++++- .../scripting/xmltags/DynamicSqlSource.java | 7 +- .../scripting/xmltags/ForEachSqlNode.java | 60 +++---- .../scripting/xmltags/StaticTextSqlNode.java | 2 +- .../ibatis/scripting/xmltags/TextSqlNode.java | 28 +--- .../ibatis/scripting/xmltags/TrimSqlNode.java | 20 +-- .../ibatis/builder/SqlSourceBuilderTest.java | 29 +--- .../xml/dynamic/DynamicSqlSourceTest.java | 33 +++- .../raw_sql_source/RawSqlSourceTest.java | 43 +++++ 15 files changed, 383 insertions(+), 251 deletions(-) create mode 100644 src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java diff --git a/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java b/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java new file mode 100644 index 00000000000..08724361b7b --- /dev/null +++ b/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java @@ -0,0 +1,154 @@ +/* + * Copyright 2009-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ibatis.builder; + +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.parsing.TokenHandler; +import org.apache.ibatis.reflection.MetaClass; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.property.PropertyTokenizer; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.JdbcType; + +public class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { + + private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; + private final List parameterMappings; + private final Class parameterType; + private final MetaObject metaParameters; + private final Object parameterObject; + private final boolean paramExists; + + public ParameterMappingTokenHandler(List parameterMappings, Configuration configuration, + Object parameterObject, Class parameterType, Map additionalParameters, boolean paramExists) { + super(configuration); + this.parameterType = parameterObject == null + ? (parameterType == null ? Object.class : parameterType) + : parameterObject.getClass(); + this.metaParameters = configuration.newMetaObject(additionalParameters); + this.parameterObject = parameterObject; + this.paramExists = paramExists; + this.parameterMappings = parameterMappings; + } + + public ParameterMappingTokenHandler(List parameterMappings, Configuration configuration, + Class parameterType, + Map additionalParameters) { + super(configuration); + this.parameterType = parameterType; + this.metaParameters = configuration.newMetaObject(additionalParameters); + this.parameterObject = null; + this.paramExists = false; + this.parameterMappings = parameterMappings; + } + + public List getParameterMappings() { + return parameterMappings; + } + + @Override + public String handleToken(String content) { + parameterMappings.add(buildParameterMapping(content)); + return "?"; + } + + private ParameterMapping buildParameterMapping(String content) { + Map propertiesMap = parseParameterMapping(content); + String property = propertiesMap.get("property"); + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(property); + Class propertyType; + if (metaParameters.hasGetter(propertyTokenizer.getName())) { // issue #448 get type from additional params + propertyType = metaParameters.getGetterType(property); + } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { + propertyType = parameterType; + } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { + propertyType = java.sql.ResultSet.class; + } else if (property == null || Map.class.isAssignableFrom(parameterType)) { + propertyType = Object.class; + } else { + MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); + if (metaClass.hasGetter(property)) { + propertyType = metaClass.getGetterType(property); + } else { + propertyType = Object.class; + } + } + ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); + Class javaType = propertyType; + String typeHandlerAlias = null; + ParameterMode mode = null; + for (Map.Entry entry : propertiesMap.entrySet()) { + String name = entry.getKey(); + String value = entry.getValue(); + if ("javaType".equals(name)) { + javaType = resolveClass(value); + builder.javaType(javaType); + } else if ("jdbcType".equals(name)) { + builder.jdbcType(resolveJdbcType(value)); + } else if ("mode".equals(name)) { + mode = resolveParameterMode(value); + builder.mode(mode); + } else if ("numericScale".equals(name)) { + builder.numericScale(Integer.valueOf(value)); + } else if ("resultMap".equals(name)) { + builder.resultMapId(value); + } else if ("typeHandler".equals(name)) { + typeHandlerAlias = value; + } else if ("jdbcTypeName".equals(name)) { + builder.jdbcTypeName(value); + } else if ("property".equals(name)) { + // Do Nothing + } else if ("expression".equals(name)) { + throw new BuilderException("Expression based parameters are not supported yet"); + } else { + throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + + "}. Valid properties are " + PARAMETER_PROPERTIES); + } + } + if (typeHandlerAlias != null) { + builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); + } + if (!ParameterMode.OUT.equals(mode) && paramExists) { + if (metaParameters.hasGetter(propertyTokenizer.getName())) { + builder.value(metaParameters.getValue(property)); + } else if (parameterObject == null) { + builder.value(null); + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + builder.value(parameterObject); + } else { + MetaObject metaObject = configuration.newMetaObject(parameterObject); + builder.value(metaObject.getValue(property)); + } + } + return builder.build(); + } + + private Map parseParameterMapping(String content) { + try { + return new ParameterExpression(content); + } catch (BuilderException ex) { + throw ex; + } catch (Exception ex) { + throw new BuilderException("Parsing error was found in mapping #{" + content + + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex); + } + } +} diff --git a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java index 9c6b3fb2765..280447d6a5a 100644 --- a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java @@ -15,41 +15,27 @@ */ package org.apache.ibatis.builder; -import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.StringTokenizer; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; -import org.apache.ibatis.parsing.GenericTokenParser; -import org.apache.ibatis.parsing.TokenHandler; -import org.apache.ibatis.reflection.MetaClass; -import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.Configuration; -import org.apache.ibatis.type.JdbcType; /** * @author Clinton Begin */ -public class SqlSourceBuilder extends BaseBuilder { +public class SqlSourceBuilder { - private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; - - public SqlSourceBuilder(Configuration configuration) { - super(configuration); + private SqlSourceBuilder() { + super(); } - public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { - ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); - GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); - String sql; - if (configuration.isShrinkWhitespacesInSql()) { - sql = parser.parse(removeExtraWhitespaces(originalSql)); - } else { - sql = parser.parse(originalSql); - } - return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); + public static SqlSource buildSqlSource(Configuration configuration, String sql, + List parameterMappings) { + return new StaticSqlSource(configuration, + configuration.isShrinkWhitespacesInSql() ? SqlSourceBuilder.removeExtraWhitespaces(sql) : sql, + parameterMappings); } public static String removeExtraWhitespaces(String original) { @@ -66,92 +52,4 @@ public static String removeExtraWhitespaces(String original) { return builder.toString(); } - private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { - - private final List parameterMappings = new ArrayList<>(); - private final Class parameterType; - private final MetaObject metaParameters; - - public ParameterMappingTokenHandler(Configuration configuration, Class parameterType, Map additionalParameters) { - super(configuration); - this.parameterType = parameterType; - this.metaParameters = configuration.newMetaObject(additionalParameters); - } - - public List getParameterMappings() { - return parameterMappings; - } - - @Override - public String handleToken(String content) { - parameterMappings.add(buildParameterMapping(content)); - return "?"; - } - - private ParameterMapping buildParameterMapping(String content) { - Map propertiesMap = parseParameterMapping(content); - String property = propertiesMap.get("property"); - Class propertyType; - if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params - propertyType = metaParameters.getGetterType(property); - } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { - propertyType = parameterType; - } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { - propertyType = java.sql.ResultSet.class; - } else if (property == null || Map.class.isAssignableFrom(parameterType)) { - propertyType = Object.class; - } else { - MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); - if (metaClass.hasGetter(property)) { - propertyType = metaClass.getGetterType(property); - } else { - propertyType = Object.class; - } - } - ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); - Class javaType = propertyType; - String typeHandlerAlias = null; - for (Map.Entry entry : propertiesMap.entrySet()) { - String name = entry.getKey(); - String value = entry.getValue(); - if ("javaType".equals(name)) { - javaType = resolveClass(value); - builder.javaType(javaType); - } else if ("jdbcType".equals(name)) { - builder.jdbcType(resolveJdbcType(value)); - } else if ("mode".equals(name)) { - builder.mode(resolveParameterMode(value)); - } else if ("numericScale".equals(name)) { - builder.numericScale(Integer.valueOf(value)); - } else if ("resultMap".equals(name)) { - builder.resultMapId(value); - } else if ("typeHandler".equals(name)) { - typeHandlerAlias = value; - } else if ("jdbcTypeName".equals(name)) { - builder.jdbcTypeName(value); - } else if ("property".equals(name)) { - // Do Nothing - } else if ("expression".equals(name)) { - throw new BuilderException("Expression based parameters are not supported yet"); - } else { - throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES); - } - } - if (typeHandlerAlias != null) { - builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); - } - return builder.build(); - } - - private Map parseParameterMapping(String content) { - try { - return new ParameterExpression(content); - } catch (BuilderException ex) { - throw ex; - } catch (Exception ex) { - throw new BuilderException("Parsing error was found in mapping #{" + content + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex); - } - } - } - } diff --git a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java index 834690abfd9..d2ae95c38f5 100644 --- a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java @@ -208,7 +208,9 @@ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBo if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); - if (boundSql.hasAdditionalParameter(propertyName)) { + if (parameterMapping.hasValue()) { + value = parameterMapping.getValue(); + } else if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; diff --git a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java index 94abab3bbb4..612cbdc4286 100644 --- a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java @@ -38,6 +38,8 @@ public class ParameterMapping { private String resultMapId; private String jdbcTypeName; private String expression; + private Object value; + private boolean hasValue; private ParameterMapping() { } @@ -99,6 +101,12 @@ public Builder expression(String expression) { return this; } + public Builder value(Object value) { + parameterMapping.value = value; + parameterMapping.hasValue = true; + return this; + } + public ParameterMapping build() { resolveTypeHandler(); validate(); @@ -207,6 +215,14 @@ public String getExpression() { return expression; } + public Object getValue() { + return value; + } + + public boolean hasValue() { + return hasValue; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder("ParameterMapping{"); @@ -220,6 +236,7 @@ public String toString() { sb.append(", resultMapId='").append(resultMapId).append('\''); sb.append(", jdbcTypeName='").append(jdbcTypeName).append('\''); sb.append(", expression='").append(expression).append('\''); + sb.append(", value='").append(value).append('\''); sb.append('}'); return sb.toString(); } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java index a9f5a91a0b8..f2256201422 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java @@ -66,18 +66,7 @@ public void setParameters(PreparedStatement ps) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { - Object value; - String propertyName = parameterMapping.getProperty(); - if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params - value = boundSql.getAdditionalParameter(propertyName); - } else if (parameterObject == null) { - value = null; - } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { - value = parameterObject; - } else { - MetaObject metaObject = configuration.newMetaObject(parameterObject); - value = metaObject.getValue(propertyName); - } + Object value = getValue(parameterMapping); TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { @@ -93,4 +82,21 @@ public void setParameters(PreparedStatement ps) { } } + private Object getValue(ParameterMapping parameterMapping) { + if (parameterMapping.hasValue()) { + return parameterMapping.getValue(); + } + String propertyName = parameterMapping.getProperty(); + if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params + return boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + return null; + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + return parameterObject; + } else { + MetaObject metaObject = configuration.newMetaObject(parameterObject); + return metaObject.getValue(propertyName); + } + } + } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java index 556712e5501..ce21535c2df 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java @@ -15,19 +15,23 @@ */ package org.apache.ibatis.scripting.defaults; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import org.apache.ibatis.builder.ParameterMappingTokenHandler; import org.apache.ibatis.builder.SqlSourceBuilder; import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.scripting.xmltags.DynamicContext; import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; import org.apache.ibatis.scripting.xmltags.SqlNode; import org.apache.ibatis.session.Configuration; /** - * Static SqlSource. It is faster than {@link DynamicSqlSource} because mappings are - * calculated during startup. + * Static SqlSource. It is faster than {@link DynamicSqlSource} because mappings are calculated during startup. * * @since 3.2.0 * @author Eduardo Macarron @@ -37,19 +41,19 @@ public class RawSqlSource implements SqlSource { private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType) { - this(configuration, getSql(configuration, rootSqlNode), parameterType); + DynamicContext context = new DynamicContext(configuration, parameterType); + rootSqlNode.apply(context); + String sql = context.getSql(); + sqlSource = SqlSourceBuilder.buildSqlSource(configuration, sql, context.getParameterMappings()); } public RawSqlSource(Configuration configuration, String sql, Class parameterType) { - SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class clazz = parameterType == null ? Object.class : parameterType; - sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); - } - - private static String getSql(Configuration configuration, SqlNode rootSqlNode) { - DynamicContext context = new DynamicContext(configuration, null); - rootSqlNode.apply(context); - return context.getSql(); + List parameterMappings = new ArrayList<>(); + ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler(parameterMappings, configuration, + clazz, new HashMap<>()); + GenericTokenParser parser = new GenericTokenParser("#{", "}", tokenHandler); + sqlSource = SqlSourceBuilder.buildSqlSource(configuration, parser.parse(sql), parameterMappings); } @Override diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java index 43d84d96729..74cda88830f 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java @@ -15,17 +15,22 @@ */ package org.apache.ibatis.scripting.xmltags; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.StringJoiner; +import org.apache.ibatis.builder.ParameterMappingTokenHandler; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.parsing.GenericTokenParser; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.session.Configuration; + import ognl.OgnlContext; import ognl.OgnlRuntime; import ognl.PropertyAccessor; -import org.apache.ibatis.reflection.MetaObject; -import org.apache.ibatis.session.Configuration; - /** * @author Clinton Begin */ @@ -38,20 +43,36 @@ public class DynamicContext { OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor()); } - private final ContextMap bindings; + protected final ContextMap bindings; private final StringJoiner sqlBuilder = new StringJoiner(" "); private int uniqueNumber = 0; - public DynamicContext(Configuration configuration, Object parameterObject) { - if (parameterObject != null && !(parameterObject instanceof Map)) { + private final Configuration configuration; + private final Object parameterObject; + private final Class parameterType; + private final boolean paramExists; + + private GenericTokenParser tokenParser; + private ParameterMappingTokenHandler tokenHandler; + + public DynamicContext(Configuration configuration, Class parameterType) { + this(configuration, null, parameterType, false); + } + + public DynamicContext(Configuration configuration, Object parameterObject, Class parameterType, boolean paramExists) { + if (parameterObject == null || parameterObject instanceof Map) { + bindings = new ContextMap(null, false); + } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass()); bindings = new ContextMap(metaObject, existsTypeHandler); - } else { - bindings = new ContextMap(null, false); } bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); + this.configuration = configuration; + this.parameterObject = parameterObject; + this.paramExists = paramExists; + this.parameterType = parameterType; } public Map getBindings() { @@ -74,6 +95,30 @@ public int getUniqueNumber() { return uniqueNumber++; } + public List getParameterMappings() { + return tokenHandler == null ? new ArrayList<>() : tokenHandler.getParameterMappings(); + } + + protected String parseParam(String sql) { + if (tokenParser == null) { + tokenHandler = new ParameterMappingTokenHandler(getParameterMappings(), configuration, parameterObject, parameterType, bindings, paramExists); + tokenParser = new GenericTokenParser("#{", "}", tokenHandler); + } + return tokenParser.parse(sql); + } + + protected Object getParameterObject() { + return parameterObject; + } + + protected Class getParameterType() { + return parameterType; + } + + protected boolean isParamExists() { + return paramExists; + } + static class ContextMap extends HashMap { private static final long serialVersionUID = 2977601501966151582L; private final MetaObject parameterMetaObject; @@ -117,7 +162,7 @@ public Object getProperty(Map context, Object target, Object name) { Object parameterObject = map.get(PARAMETER_OBJECT_KEY); if (parameterObject instanceof Map) { - return ((Map)parameterObject).get(name); + return ((Map) parameterObject).get(name); } return null; diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java index fce0bfa5396..03ab703b2b7 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java @@ -35,11 +35,10 @@ public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { @Override public BoundSql getBoundSql(Object parameterObject) { - DynamicContext context = new DynamicContext(configuration, parameterObject); + DynamicContext context = new DynamicContext(configuration, parameterObject, null, true); rootSqlNode.apply(context); - SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); - Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); - SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + String sql = context.getSql(); + SqlSource sqlSource = SqlSourceBuilder.buildSqlSource(configuration, sql, context.getParameterMappings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java index 1c7ebbb5f1b..0bf0959d996 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java @@ -15,9 +15,11 @@ */ package org.apache.ibatis.scripting.xmltags; +import java.util.List; import java.util.Map; import java.util.Optional; +import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.session.Configuration; @@ -74,33 +76,30 @@ public boolean apply(DynamicContext context) { applyOpen(context); int i = 0; for (Object o : iterable) { - DynamicContext oldContext = context; + DynamicContext scopedContext; if (first || separator == null) { - context = new PrefixedContext(context, ""); + scopedContext = new PrefixedContext(context, ""); } else { - context = new PrefixedContext(context, separator); + scopedContext = new PrefixedContext(context, separator); } - int uniqueNumber = context.getUniqueNumber(); + int uniqueNumber = scopedContext.getUniqueNumber(); // Issue #709 if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry mapEntry = (Map.Entry) o; - applyIndex(context, mapEntry.getKey(), uniqueNumber); - applyItem(context, mapEntry.getValue(), uniqueNumber); + applyIndex(scopedContext, mapEntry.getKey(), uniqueNumber); + applyItem(scopedContext, mapEntry.getValue(), uniqueNumber); } else { - applyIndex(context, i, uniqueNumber); - applyItem(context, o, uniqueNumber); + applyIndex(scopedContext, i, uniqueNumber); + applyItem(scopedContext, o, uniqueNumber); } - contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); + contents.apply(new FilteredDynamicContext(configuration, scopedContext, index, item, uniqueNumber)); if (first) { - first = !((PrefixedContext) context).isPrefixApplied(); + first = !((PrefixedContext) scopedContext).isPrefixApplied(); } - context = oldContext; i++; } applyClose(context); - context.getBindings().remove(item); - context.getBindings().remove(index); return true; } @@ -141,21 +140,12 @@ private static class FilteredDynamicContext extends DynamicContext { private final String item; public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) { - super(configuration, null); + super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); this.delegate = delegate; this.index = i; this.itemIndex = itemIndex; this.item = item; - } - - @Override - public Map getBindings() { - return delegate.getBindings(); - } - - @Override - public void bind(String name, Object value) { - delegate.bind(name, value); + this.bindings.putAll(delegate.getBindings()); } @Override @@ -181,6 +171,10 @@ public int getUniqueNumber() { return delegate.getUniqueNumber(); } + @Override + public List getParameterMappings() { + return delegate.getParameterMappings(); + } } @@ -190,26 +184,17 @@ private class PrefixedContext extends DynamicContext { private boolean prefixApplied; public PrefixedContext(DynamicContext delegate, String prefix) { - super(configuration, null); + super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); this.delegate = delegate; this.prefix = prefix; this.prefixApplied = false; + this.bindings.putAll(delegate.getBindings()); } public boolean isPrefixApplied() { return prefixApplied; } - @Override - public Map getBindings() { - return delegate.getBindings(); - } - - @Override - public void bind(String name, Object value) { - delegate.bind(name, value); - } - @Override public void appendSql(String sql) { if (!prefixApplied && sql != null && sql.trim().length() > 0) { @@ -228,6 +213,11 @@ public String getSql() { public int getUniqueNumber() { return delegate.getUniqueNumber(); } + + @Override + public List getParameterMappings() { + return delegate.getParameterMappings(); + } } } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java index 9a938d5d0f2..e1e9f504350 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java @@ -27,7 +27,7 @@ public StaticTextSqlNode(String text) { @Override public boolean apply(DynamicContext context) { - context.appendSql(text); + context.appendSql(context.parseParam(text)); return true; } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java index bce1ab1e263..5bcc749223b 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java @@ -15,11 +15,8 @@ */ package org.apache.ibatis.scripting.xmltags; -import java.util.regex.Pattern; - import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.parsing.TokenHandler; -import org.apache.ibatis.scripting.ScriptingException; import org.apache.ibatis.type.SimpleTypeRegistry; /** @@ -27,15 +24,9 @@ */ public class TextSqlNode implements SqlNode { private final String text; - private final Pattern injectionFilter; public TextSqlNode(String text) { - this(text, null); - } - - public TextSqlNode(String text, Pattern injectionFilter) { this.text = text; - this.injectionFilter = injectionFilter; } public boolean isDynamic() { @@ -47,8 +38,8 @@ public boolean isDynamic() { @Override public boolean apply(DynamicContext context) { - GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); - context.appendSql(parser.parse(text)); + GenericTokenParser parser = createParser(new BindingTokenParser(context)); + context.appendSql(context.parseParam(parser.parse(text))); return true; } @@ -59,11 +50,9 @@ private GenericTokenParser createParser(TokenHandler handler) { private static class BindingTokenParser implements TokenHandler { private DynamicContext context; - private Pattern injectionFilter; - public BindingTokenParser(DynamicContext context, Pattern injectionFilter) { + public BindingTokenParser(DynamicContext context) { this.context = context; - this.injectionFilter = injectionFilter; } @Override @@ -75,15 +64,8 @@ public String handleToken(String content) { context.getBindings().put("value", parameter); } Object value = OgnlCache.getValue(content, context.getBindings()); - String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" - checkInjection(srtValue); - return srtValue; - } - - private void checkInjection(String value) { - if (injectionFilter != null && !injectionFilter.matcher(value).matches()) { - throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern()); - } + // issue #274 return "" instead of "null" + return value == null ? "" : String.valueOf(value); } } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java index 7860214ee15..b6b720577c3 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java @@ -19,9 +19,9 @@ import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.StringTokenizer; +import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.session.Configuration; /** @@ -76,11 +76,12 @@ private class FilteredDynamicContext extends DynamicContext { private StringBuilder sqlBuffer; public FilteredDynamicContext(DynamicContext delegate) { - super(configuration, null); + super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); this.delegate = delegate; this.prefixApplied = false; this.suffixApplied = false; this.sqlBuffer = new StringBuilder(); + this.bindings.putAll(delegate.getBindings()); } public void applyAll() { @@ -93,16 +94,6 @@ public void applyAll() { delegate.appendSql(sqlBuffer.toString()); } - @Override - public Map getBindings() { - return delegate.getBindings(); - } - - @Override - public void bind(String name, Object value) { - delegate.bind(name, value); - } - @Override public int getUniqueNumber() { return delegate.getUniqueNumber(); @@ -118,6 +109,11 @@ public String getSql() { return delegate.getSql(); } + @Override + public List getParameterMappings() { + return delegate.getParameterMappings(); + } + private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { prefixApplied = true; diff --git a/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java b/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java index 871e2b88a00..12f48ce8b7e 100644 --- a/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java +++ b/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java @@ -15,41 +15,16 @@ */ package org.apache.ibatis.builder; -import org.apache.ibatis.mapping.BoundSql; -import org.apache.ibatis.mapping.SqlSource; -import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class SqlSourceBuilderTest { +class SqlSourceBuilderTest { - private static Configuration configuration; - private static SqlSourceBuilder sqlSourceBuilder; private final String sqlFromXml = "\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "; - @BeforeEach - void setUp() { - configuration = new Configuration(); - - sqlSourceBuilder = new SqlSourceBuilder(configuration); - } - - @Test - void testShrinkWhitespacesInSqlIsFalse() { - SqlSource sqlSource = sqlSourceBuilder.parse(sqlFromXml, null, null); - BoundSql boundSql = sqlSource.getBoundSql(null); - String actual = boundSql.getSql(); - Assertions.assertEquals(sqlFromXml, actual); - } - @Test void testShrinkWhitespacesInSqlIsTrue() { - configuration.setShrinkWhitespacesInSql(true); - SqlSource sqlSource = sqlSourceBuilder.parse(sqlFromXml, null, null); - BoundSql boundSql = sqlSource.getBoundSql(null); - String actual = boundSql.getSql(); - + String actual = SqlSourceBuilder.removeExtraWhitespaces(sqlFromXml); String shrankWhitespacesInSql = "SELECT * FROM user WHERE user_id = 1"; Assertions.assertEquals(shrankWhitespacesInSql, actual); } diff --git a/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java b/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java index c2c6033179a..a68e0ac5ff6 100644 --- a/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java +++ b/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.apache.ibatis.BaseDataTest; import org.apache.ibatis.io.Resources; @@ -36,6 +37,7 @@ import org.apache.ibatis.scripting.xmltags.MixedSqlNode; import org.apache.ibatis.scripting.xmltags.SetSqlNode; import org.apache.ibatis.scripting.xmltags.SqlNode; +import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode; import org.apache.ibatis.scripting.xmltags.TextSqlNode; import org.apache.ibatis.scripting.xmltags.WhereSqlNode; import org.apache.ibatis.session.Configuration; @@ -43,6 +45,9 @@ import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class DynamicSqlSourceTest extends BaseDataTest { @@ -324,9 +329,9 @@ void shouldIterateOnceForEachItemInCollection() throws Exception { BoundSql boundSql = source.getBoundSql(parameterObject); assertEquals(expected, boundSql.getSql()); assertEquals(3, boundSql.getParameterMappings().size()); - assertEquals("__frch_item_0", boundSql.getParameterMappings().get(0).getProperty()); - assertEquals("__frch_item_1", boundSql.getParameterMappings().get(1).getProperty()); - assertEquals("__frch_item_2", boundSql.getParameterMappings().get(2).getProperty()); + assertEquals("item", boundSql.getParameterMappings().get(0).getProperty()); + assertEquals("item", boundSql.getParameterMappings().get(1).getProperty()); + assertEquals("item", boundSql.getParameterMappings().get(2).getProperty()); } @Test @@ -371,9 +376,9 @@ void shouldPerformStrictMatchOnForEachVariableSubstitution() throws Exception { BoundSql boundSql = source.getBoundSql(param); assertEquals(4, boundSql.getParameterMappings().size()); assertEquals("uuu.u", boundSql.getParameterMappings().get(0).getProperty()); - assertEquals("__frch_u_0.id", boundSql.getParameterMappings().get(1).getProperty()); - assertEquals("__frch_u_0", boundSql.getParameterMappings().get(2).getProperty()); - assertEquals("__frch_u_0", boundSql.getParameterMappings().get(3).getProperty()); + assertEquals("u.id", boundSql.getParameterMappings().get(1).getProperty()); + assertEquals("u", boundSql.getParameterMappings().get(2).getProperty()); + assertEquals("u", boundSql.getParameterMappings().get(3).getProperty()); } private DynamicSqlSource createDynamicSqlSource(SqlNode... contents) throws IOException, SQLException { @@ -412,4 +417,20 @@ public void setId(String property) { } } + @MethodSource + @ParameterizedTest + void testShrinkWhitespacesInSql(SqlNode input, boolean shrinkWhitespaces, String expected) { + Configuration config = new Configuration(); + config.setShrinkWhitespacesInSql(shrinkWhitespaces); + String actual = new DynamicSqlSource(config, input).getBoundSql(null).getSql(); + assertEquals(expected, actual); + } + + static Stream testShrinkWhitespacesInSql() { + return Stream.of( + Arguments.arguments(new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "), false, + "SELECT * \n FROM user\n \t WHERE user_id = 1"), + Arguments.arguments(new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t"), true, + "SELECT * FROM user WHERE user_id = 1")); + } } diff --git a/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java b/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java index 4e6572267ab..3a312013c74 100644 --- a/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java +++ b/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java @@ -15,19 +15,28 @@ */ package org.apache.ibatis.submitted.raw_sql_source; +import static org.junit.jupiter.api.Assertions.*; + import java.io.Reader; +import java.util.stream.Stream; import org.apache.ibatis.BaseDataTest; import org.apache.ibatis.io.Resources; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.scripting.defaults.RawSqlSource; import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; +import org.apache.ibatis.scripting.xmltags.SqlNode; +import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode; +import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class RawSqlSourceTest { @@ -72,4 +81,38 @@ private void test(String statement, Class sqlSource) { } } + @MethodSource + @ParameterizedTest + void testShrinkWhitespacesInSql(String input, boolean shrinkWhitespaces, String expected) { + Configuration config = new Configuration(); + config.setShrinkWhitespacesInSql(shrinkWhitespaces); + String actual = new RawSqlSource(config, input, null).getBoundSql(null).getSql(); + assertEquals(expected, actual); + } + + static Stream testShrinkWhitespacesInSql() { + return Stream.of( + Arguments.arguments("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t ", false, + "\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "), + Arguments.arguments("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t", true, + "SELECT * FROM user WHERE user_id = 1")); + } + + @MethodSource + @ParameterizedTest + void testShrinkWhitespacesInSql_SqlNode(SqlNode input, boolean shrinkWhitespaces, String expected) { + Configuration config = new Configuration(); + config.setShrinkWhitespacesInSql(shrinkWhitespaces); + String actual = new RawSqlSource(config, input, null).getBoundSql(null).getSql(); + assertEquals(expected, actual); + } + + static Stream testShrinkWhitespacesInSql_SqlNode() { + return Stream.of( + Arguments.arguments( + new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "), false, + "SELECT * \n FROM user\n \t WHERE user_id = 1"), + Arguments.arguments(new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t"), true, + "SELECT * FROM user WHERE user_id = 1")); + } } From e7f76227fcab374e4e0e5b6fef32430212039b39 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Sat, 17 Dec 2022 12:51:02 +0900 Subject: [PATCH 003/133] Stop rewriting item/index variables inside foreach Although it is not the goal, this could improve performance when there are many foreach iterations. --- .../scripting/xmltags/DynamicContext.java | 5 -- .../scripting/xmltags/ForEachSqlNode.java | 81 +++---------------- .../ibatis/scripting/xmltags/TrimSqlNode.java | 5 -- 3 files changed, 12 insertions(+), 79 deletions(-) diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java index 74cda88830f..cd4fb9ba11f 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java @@ -45,7 +45,6 @@ public class DynamicContext { protected final ContextMap bindings; private final StringJoiner sqlBuilder = new StringJoiner(" "); - private int uniqueNumber = 0; private final Configuration configuration; private final Object parameterObject; @@ -91,10 +90,6 @@ public String getSql() { return sqlBuilder.toString().trim(); } - public int getUniqueNumber() { - return uniqueNumber++; - } - public List getParameterMappings() { return tokenHandler == null ? new ArrayList<>() : tokenHandler.getParameterMappings(); } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java index 0bf0959d996..066bfb6da3c 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java @@ -20,14 +20,12 @@ import java.util.Optional; import org.apache.ibatis.mapping.ParameterMapping; -import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.session.Configuration; /** * @author Clinton Begin */ public class ForEachSqlNode implements SqlNode { - public static final String ITEM_PREFIX = "__frch_"; private final ExpressionEvaluator evaluator; private final String collectionExpression; @@ -44,14 +42,16 @@ public class ForEachSqlNode implements SqlNode { * @deprecated Since 3.5.9, use the {@link #ForEachSqlNode(Configuration, SqlNode, String, Boolean, String, String, String, String, String)}. */ @Deprecated - public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) { + public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, + String item, String open, String close, String separator) { this(configuration, contents, collectionExpression, null, index, item, open, close, separator); } /** * @since 3.5.9 */ - public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable, String index, String item, String open, String close, String separator) { + public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable, + String index, String item, String open, String close, String separator) { this.evaluator = new ExpressionEvaluator(); this.collectionExpression = collectionExpression; this.nullable = nullable; @@ -68,7 +68,7 @@ public ForEachSqlNode(Configuration configuration, SqlNode contents, String coll public boolean apply(DynamicContext context) { Map bindings = context.getBindings(); final Iterable iterable = evaluator.evaluateIterable(collectionExpression, bindings, - Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach)); + Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach)); if (iterable == null || !iterable.iterator().hasNext()) { return true; } @@ -82,18 +82,17 @@ public boolean apply(DynamicContext context) { } else { scopedContext = new PrefixedContext(context, separator); } - int uniqueNumber = scopedContext.getUniqueNumber(); // Issue #709 if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry mapEntry = (Map.Entry) o; - applyIndex(scopedContext, mapEntry.getKey(), uniqueNumber); - applyItem(scopedContext, mapEntry.getValue(), uniqueNumber); + applyIndex(scopedContext, mapEntry.getKey()); + applyItem(scopedContext, mapEntry.getValue()); } else { - applyIndex(scopedContext, i, uniqueNumber); - applyItem(scopedContext, o, uniqueNumber); + applyIndex(scopedContext, i); + applyItem(scopedContext, o); } - contents.apply(new FilteredDynamicContext(configuration, scopedContext, index, item, uniqueNumber)); + contents.apply(scopedContext); if (first) { first = !((PrefixedContext) scopedContext).isPrefixApplied(); } @@ -103,17 +102,15 @@ public boolean apply(DynamicContext context) { return true; } - private void applyIndex(DynamicContext context, Object o, int i) { + private void applyIndex(DynamicContext context, Object o) { if (index != null) { context.bind(index, o); - context.bind(itemizeItem(index, i), o); } } - private void applyItem(DynamicContext context, Object o, int i) { + private void applyItem(DynamicContext context, Object o) { if (item != null) { context.bind(item, o); - context.bind(itemizeItem(item, i), o); } } @@ -129,55 +126,6 @@ private void applyClose(DynamicContext context) { } } - private static String itemizeItem(String item, int i) { - return ITEM_PREFIX + item + "_" + i; - } - - private static class FilteredDynamicContext extends DynamicContext { - private final DynamicContext delegate; - private final int index; - private final String itemIndex; - private final String item; - - public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) { - super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); - this.delegate = delegate; - this.index = i; - this.itemIndex = itemIndex; - this.item = item; - this.bindings.putAll(delegate.getBindings()); - } - - @Override - public String getSql() { - return delegate.getSql(); - } - - @Override - public void appendSql(String sql) { - GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> { - String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index)); - if (itemIndex != null && newContent.equals(content)) { - newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index)); - } - return "#{" + newContent + "}"; - }); - - delegate.appendSql(parser.parse(sql)); - } - - @Override - public int getUniqueNumber() { - return delegate.getUniqueNumber(); - } - - @Override - public List getParameterMappings() { - return delegate.getParameterMappings(); - } - } - - private class PrefixedContext extends DynamicContext { private final DynamicContext delegate; private final String prefix; @@ -209,11 +157,6 @@ public String getSql() { return delegate.getSql(); } - @Override - public int getUniqueNumber() { - return delegate.getUniqueNumber(); - } - @Override public List getParameterMappings() { return delegate.getParameterMappings(); diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java index b6b720577c3..55017f87963 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java @@ -94,11 +94,6 @@ public void applyAll() { delegate.appendSql(sqlBuffer.toString()); } - @Override - public int getUniqueNumber() { - return delegate.getUniqueNumber(); - } - @Override public void appendSql(String sql) { sqlBuffer.append(sql); From 78276f3933f69e1d3b53f15863dafb7aaf3240aa Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Wed, 21 Dec 2022 05:05:41 +0900 Subject: [PATCH 004/133] Resolve type handler based on Type instead of Class --- .../apache/ibatis/binding/MapperMethod.java | 2 +- .../apache/ibatis/builder/BaseBuilder.java | 41 ++- .../builder/MapperBuilderAssistant.java | 82 ++++- .../ibatis/builder/SqlSourceBuilder.java | 119 +++++-- .../annotation/MapperAnnotationBuilder.java | 54 +-- .../builder/annotation/ProviderSqlSource.java | 8 +- .../ibatis/builder/xml/XMLMapperBuilder.java | 8 +- .../builder/xml/XMLStatementBuilder.java | 37 +- .../resultset/DefaultResultSetHandler.java | 86 +++-- .../ibatis/mapping/MappedStatement.java | 11 + .../ibatis/mapping/ParameterMapping.java | 26 +- .../apache/ibatis/mapping/ResultMapping.java | 11 +- .../reflection/DefaultReflectorFactory.java | 5 +- .../apache/ibatis/reflection/MetaClass.java | 68 ++-- .../apache/ibatis/reflection/MetaObject.java | 10 + .../ibatis/reflection/ParamNameResolver.java | 52 ++- .../apache/ibatis/reflection/Reflector.java | 74 ++-- .../ibatis/reflection/ReflectorFactory.java | 6 +- .../reflection/TypeParameterResolver.java | 33 ++ .../reflection/wrapper/BeanWrapper.java | 32 ++ .../ibatis/reflection/wrapper/MapWrapper.java | 16 + .../reflection/wrapper/ObjectWrapper.java | 10 + .../ibatis/scripting/LanguageDriver.java | 9 + .../defaults/DefaultParameterHandler.java | 124 ++++++- .../scripting/defaults/RawLanguageDriver.java | 5 +- .../scripting/defaults/RawSqlSource.java | 13 +- .../scripting/xmltags/DynamicSqlSource.java | 9 +- .../scripting/xmltags/XMLLanguageDriver.java | 15 +- .../scripting/xmltags/XMLScriptBuilder.java | 9 +- .../AutoMappingUnknownColumnBehavior.java | 14 +- .../apache/ibatis/session/Configuration.java | 7 + .../apache/ibatis/type/BaseTypeHandler.java | 2 +- .../ibatis/type/ConflictedTypeHandler.java | 76 +++++ .../type/DefaultTypeHandlerResolver.java | 36 ++ .../ibatis/type/TypeHandlerRegistry.java | 243 +++++++++---- .../ibatis/type/TypeHandlerResolver.java | 25 ++ .../org/apache/ibatis/type/TypeReference.java | 8 +- .../ibatis/builder/XmlMapperBuilderTest.java | 5 +- .../reflection/ParamNameResolverTest.java | 63 ++++ .../ibatis/reflection/ReflectorTest.java | 40 ++- .../reflection/TypeParameterResolverTest.java | 43 +++ .../defaults/DefaultParameterHandlerTest.java | 2 + .../BooleanCharTypeHandler.java | 52 +++ .../BooleanIntTypeHandler.java | 51 +++ .../handle_by_jdbc_type/BoolsBean.java | 57 ++++ .../HandlerByJdbcTypeTest.java | 96 ++++++ .../submitted/handle_by_jdbc_type/Mapper.java | 39 +++ .../language/VelocitySqlSourceBuilder.java | 10 +- .../maptypehandler/MapTypeHandlerTest.java | 2 + .../CsvTypeHandler.java | 86 +++++ .../FuzzyBean.java | 37 ++ .../GloballyRegisteredHandlerMapper.java | 42 +++ ...lyRegisteredTypeHandlerResolutionTest.java | 190 +++++++++++ .../LocallySpecifiedHandlerMapper.java | 92 +++++ ...llySpecifiedTypeHandlerResolutionTest.java | 318 ++++++++++++++++++ .../TypeAwareTypeHandler.java | 89 +++++ .../typebasedtypehandlerresolution/User.java | 86 +++++ .../typehandler/TypeHandlerTest.java | 4 + .../unknownobject/UnknownObjectTest.java | 24 +- ...ricTypeSupportedInHierarchiesTestCase.java | 49 --- .../ibatis/type/TypeHandlerRegistryTest.java | 58 +++- .../apache/ibatis/type/TypeReferenceTest.java | 55 +++ .../org/apache/ibatis/builder/BlogMapper.xml | 2 +- .../handle_by_jdbc_type/CreateDB.sql | 25 ++ .../submitted/handle_by_jdbc_type/Mapper.xml | 41 +++ .../handle_by_jdbc_type/mybatis-config.xml | 51 +++ .../CreateDB.sql | 32 ++ .../GloballyRegisteredHandlerMapper.xml | 51 +++ .../LocallySpecifiedHandlerMapper.xml | 105 ++++++ .../mybatis-config.xml | 43 +++ 70 files changed, 2945 insertions(+), 381 deletions(-) create mode 100644 src/main/java/org/apache/ibatis/type/ConflictedTypeHandler.java create mode 100644 src/main/java/org/apache/ibatis/type/DefaultTypeHandlerResolver.java create mode 100644 src/main/java/org/apache/ibatis/type/TypeHandlerResolver.java create mode 100644 src/test/java/org/apache/ibatis/reflection/ParamNameResolverTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/BooleanCharTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/BooleanIntTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/BoolsBean.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/HandlerByJdbcTypeTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/Mapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/CsvTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/FuzzyBean.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/GloballyRegisteredHandlerMapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/GloballyRegisteredTypeHandlerResolutionTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/LocallySpecifiedHandlerMapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/LocallySpecifiedTypeHandlerResolutionTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/TypeAwareTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/User.java delete mode 100644 src/test/java/org/apache/ibatis/type/GenericTypeSupportedInHierarchiesTestCase.java create mode 100644 src/test/java/org/apache/ibatis/type/TypeReferenceTest.java create mode 100644 src/test/resources/org/apache/ibatis/submitted/handle_by_jdbc_type/CreateDB.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/handle_by_jdbc_type/Mapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/handle_by_jdbc_type/mybatis-config.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/CreateDB.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/GloballyRegisteredHandlerMapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/LocallySpecifiedHandlerMapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/mybatis-config.xml diff --git a/src/main/java/org/apache/ibatis/binding/MapperMethod.java b/src/main/java/org/apache/ibatis/binding/MapperMethod.java index 17b6cd9f863..f0eaf317ed0 100644 --- a/src/main/java/org/apache/ibatis/binding/MapperMethod.java +++ b/src/main/java/org/apache/ibatis/binding/MapperMethod.java @@ -302,7 +302,7 @@ public MethodSignature(Configuration configuration, Class mapperInterface, Me this.returnsMap = this.mapKey != null; this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); - this.paramNameResolver = new ParamNameResolver(configuration, method); + this.paramNameResolver = new ParamNameResolver(configuration, method, mapperInterface); } public Object convertArgsToSqlCommandParam(Object[] args) { diff --git a/src/main/java/org/apache/ibatis/builder/BaseBuilder.java b/src/main/java/org/apache/ibatis/builder/BaseBuilder.java index 7e7a443726b..43efd8f5446 100644 --- a/src/main/java/org/apache/ibatis/builder/BaseBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/BaseBuilder.java @@ -15,6 +15,7 @@ */ package org.apache.ibatis.builder; +import java.lang.reflect.Type; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -119,30 +120,34 @@ protected Class resolveClass(String alias) { } } + @Deprecated protected TypeHandler resolveTypeHandler(Class javaType, String typeHandlerAlias) { - if (typeHandlerAlias == null) { - return null; - } - Class type = resolveClass(typeHandlerAlias); - if (type != null && !TypeHandler.class.isAssignableFrom(type)) { - throw new BuilderException("Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface"); - } - @SuppressWarnings("unchecked") // already verified it is a TypeHandler - Class> typeHandlerType = (Class>) type; - return resolveTypeHandler(javaType, typeHandlerType); + return resolveTypeHandler(null, null, javaType, null, typeHandlerAlias); } + @Deprecated protected TypeHandler resolveTypeHandler(Class javaType, Class> typeHandlerType) { - if (typeHandlerType == null) { - return null; + return resolveTypeHandler(null, null, javaType, null, typeHandlerType); + } + + protected TypeHandler resolveTypeHandler(Class parameterType, String propertyName, Type propertyType, + JdbcType jdbcType, String typeHandlerAlias) { + Class> typeHandlerType = null; + typeHandlerType = resolveClass(typeHandlerAlias); + if (typeHandlerType != null && !TypeHandler.class.isAssignableFrom(typeHandlerType)) { + throw new BuilderException("Type " + typeHandlerType.getName() + + " is not a valid TypeHandler because it does not implement TypeHandler interface"); } - // javaType ignored for injected handlers see issue #746 for full detail - TypeHandler handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType); - if (handler == null) { - // not in registry, create a new one - handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType); + return resolveTypeHandler(parameterType, propertyName, propertyType, jdbcType, typeHandlerType); + } + + protected TypeHandler resolveTypeHandler(Class parameterType, String propertyName, Type propertyType, + JdbcType jdbcType, Class> typeHandlerType) { + if (typeHandlerType == null && jdbcType == null) { + return null; } - return handler; + return configuration.getTypeHandlerResolver().resolve(parameterType, propertyType, propertyName, jdbcType, + typeHandlerType); } protected Class resolveAlias(String alias) { diff --git a/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java b/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java index 4c9341a16a9..1605c899cf3 100644 --- a/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java +++ b/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java @@ -15,12 +15,15 @@ */ package org.apache.ibatis.builder; +import java.lang.reflect.Type; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; @@ -44,10 +47,12 @@ import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.reflection.MetaClass; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; +import org.apache.ibatis.util.MapUtil; /** * @author Clinton Begin @@ -162,7 +167,7 @@ public ParameterMapping buildParameterMapping( // Class parameterType = parameterMapBuilder.type(); Class javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType); - TypeHandler typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); + TypeHandler typeHandlerInstance = resolveTypeHandler(parameterType, property, javaTypeClass, jdbcType, typeHandler); return new ParameterMapping.Builder(configuration, property, javaTypeClass) .jdbcType(jdbcType) @@ -241,6 +246,49 @@ public Discriminator buildDiscriminator( return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build(); } + /** + * @param id + * the id + * @param sqlSource + * the sql source + * @param statementType + * the statement type + * @param sqlCommandType + * the sql command type + * @param fetchSize + * the fetch size + * @param timeout + * the timeout + * @param parameterMap + * the parameter map + * @param parameterType + * the parameter type + * @param resultMap + * the result map + * @param resultType + * the result type + * @param resultSetType + * the result set type + * @param flushCache + * the flush cache + * @param useCache + * the use cache + * @param resultOrdered + * the result ordered + * @param keyGenerator + * the key generator + * @param keyProperty + * the key property + * @param keyColumn + * the key column + * @param databaseId + * the database id + * @param lang + * the lang + * @param paramNameResolver + * the param name resolver + * @return the mapped statement + */ public MappedStatement addMappedStatement( String id, SqlSource sqlSource, @@ -262,7 +310,8 @@ public MappedStatement addMappedStatement( String databaseId, LanguageDriver lang, String resultSets, - boolean dirtySelect) { + boolean dirtySelect, + ParamNameResolver paramNameResolver) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); @@ -287,7 +336,8 @@ public MappedStatement addMappedStatement( .flushCacheRequired(flushCache) .useCache(useCache) .cache(currentCache) - .dirtySelect(dirtySelect); + .dirtySelect(dirtySelect) + .paramNameResolver(paramNameResolver); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { @@ -351,7 +401,7 @@ public MappedStatement addMappedStatement(String id, SqlSource sqlSource, Statem id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, - keyColumn, databaseId, lang, null, false); + keyColumn, databaseId, lang, null, false, null); } public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, @@ -436,15 +486,15 @@ public ResultMapping buildResultMapping( String resultSet, String foreignColumn, boolean lazy) { - Class javaTypeClass = resolveResultJavaType(resultType, property, javaType); - TypeHandler typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); + Entry> setterType = resolveSetterType(resultType, property, javaType); + TypeHandler typeHandlerInstance = resolveTypeHandler(resultType, property, setterType.getKey(), jdbcType, typeHandler); List composites; if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) { composites = Collections.emptyList(); } else { composites = parseCompositeColumnName(column); } - return new ResultMapping.Builder(configuration, property, column, javaTypeClass) + return new ResultMapping.Builder(configuration, property, column, setterType.getValue()) .jdbcType(jdbcType) .nestedQueryId(applyCurrentNamespace(nestedSelect, true)) .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)) @@ -538,19 +588,19 @@ private List parseCompositeColumnName(String columnName) { return composites; } - private Class resolveResultJavaType(Class resultType, String property, Class javaType) { - if (javaType == null && property != null) { + private Entry> resolveSetterType(Class resultType, String property, Class javaType) { + if (javaType != null) { + return MapUtil.entry(javaType, javaType); + } + if (property != null) { + MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory()); try { - MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory()); - javaType = metaResultType.getSetterType(property); + return metaResultType.getGenericSetterType(property); } catch (Exception e) { - // ignore, following null check statement will deal with the situation + // Not all property types are resolvable. } } - if (javaType == null) { - javaType = Object.class; - } - return javaType; + return MapUtil.entry(Object.class, Object.class); } private Class resolveParameterJavaType(Class resultType, String property, Class javaType, JdbcType jdbcType) { diff --git a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java index 9c6b3fb2765..263b885585c 100644 --- a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java @@ -15,19 +15,25 @@ */ package org.apache.ibatis.builder; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.StringTokenizer; +import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.parsing.TokenHandler; import org.apache.ibatis.reflection.MetaClass; import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.ParamNameResolver; +import org.apache.ibatis.reflection.property.PropertyTokenizer; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeHandler; /** * @author Clinton Begin @@ -41,7 +47,13 @@ public SqlSourceBuilder(Configuration configuration) { } public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { - ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); + return parse(originalSql, parameterType, additionalParameters, null); + } + + public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters, + ParamNameResolver paramNameResolver) { + ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, + additionalParameters, paramNameResolver); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql; if (configuration.isShrinkWhitespacesInSql()) { @@ -71,11 +83,17 @@ private static class ParameterMappingTokenHandler extends BaseBuilder implements private final List parameterMappings = new ArrayList<>(); private final Class parameterType; private final MetaObject metaParameters; + private final ParamNameResolver paramNameResolver; + + private Type genericType = null; + private TypeHandler typeHandler = null; - public ParameterMappingTokenHandler(Configuration configuration, Class parameterType, Map additionalParameters) { + public ParameterMappingTokenHandler(Configuration configuration, Class parameterType, + Map additionalParameters, ParamNameResolver paramNameResolver) { super(configuration); this.parameterType = parameterType; this.metaParameters = configuration.newMetaObject(additionalParameters); + this.paramNameResolver = paramNameResolver; } public List getParameterMappings() { @@ -90,59 +108,88 @@ public String handleToken(String content) { private ParameterMapping buildParameterMapping(String content) { Map propertiesMap = parseParameterMapping(content); - String property = propertiesMap.get("property"); - Class propertyType; - if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params - propertyType = metaParameters.getGetterType(property); - } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { - propertyType = parameterType; - } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { - propertyType = java.sql.ResultSet.class; - } else if (property == null || Map.class.isAssignableFrom(parameterType)) { - propertyType = Object.class; - } else { - MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); - if (metaClass.hasGetter(property)) { - propertyType = metaClass.getGetterType(property); - } else { - propertyType = Object.class; - } + final String property = propertiesMap.remove("property"); + final JdbcType jdbcType = resolveJdbcType(propertiesMap.remove("jdbcType")); + final String typeHandlerAlias = propertiesMap.remove("typeHandler"); + ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, (Class) null); + builder.jdbcType(jdbcType); + final Class javaType = figureOutJavaType(propertiesMap, property, jdbcType); + builder.javaType(javaType); + if (genericType == null) { + genericType = javaType; + } + if (typeHandler == null || typeHandlerAlias != null) { + typeHandler = resolveTypeHandler(parameterType, property, genericType, jdbcType, typeHandlerAlias); } - ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); - Class javaType = propertyType; - String typeHandlerAlias = null; + builder.typeHandler(typeHandler); + for (Map.Entry entry : propertiesMap.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); - if ("javaType".equals(name)) { - javaType = resolveClass(value); - builder.javaType(javaType); - } else if ("jdbcType".equals(name)) { - builder.jdbcType(resolveJdbcType(value)); - } else if ("mode".equals(name)) { + if ("mode".equals(name)) { builder.mode(resolveParameterMode(value)); } else if ("numericScale".equals(name)) { builder.numericScale(Integer.valueOf(value)); } else if ("resultMap".equals(name)) { builder.resultMapId(value); - } else if ("typeHandler".equals(name)) { - typeHandlerAlias = value; } else if ("jdbcTypeName".equals(name)) { builder.jdbcTypeName(value); - } else if ("property".equals(name)) { - // Do Nothing } else if ("expression".equals(name)) { throw new BuilderException("Expression based parameters are not supported yet"); } else { - throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES); + throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + + "}. Valid properties are " + PARAMETER_PROPERTIES); } } - if (typeHandlerAlias != null) { - builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); - } return builder.build(); } + private Class figureOutJavaType(Map propertiesMap, String property, JdbcType jdbcType) { + Class javaType = resolveClass(propertiesMap.remove("javaType")); + if (javaType != null) { + return javaType; + } + if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params + return metaParameters.getGetterType(property); + } + typeHandler = resolveTypeHandler(parameterType, null, null, jdbcType, (Class>)null); + if (typeHandler != null) { + return parameterType; + } + if (JdbcType.CURSOR.equals(jdbcType)) { + return java.sql.ResultSet.class; + } + if (paramNameResolver != null && ParamMap.class.equals(parameterType)) { + Type actualParamType = paramNameResolver.getType(property); + if (actualParamType instanceof Type) { + MetaClass metaClass = MetaClass.forClass(actualParamType, configuration.getReflectorFactory()); + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(property); + String multiParamsPropertyName; + if (propertyTokenizer.hasNext()) { + multiParamsPropertyName = propertyTokenizer.getChildren(); + if (metaClass.hasGetter(multiParamsPropertyName)) { + Entry> getterType = metaClass.getGenericGetterType(multiParamsPropertyName); + genericType = getterType.getKey(); + return getterType.getValue(); + } + } else { + genericType = actualParamType; + } + } + return Object.class; + } + if (Map.class.isAssignableFrom(parameterType)) { + return Object.class; + } + MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); + if (metaClass.hasGetter(property)) { + Entry> getterType = metaClass.getGenericGetterType(property); + genericType = getterType.getKey(); + return getterType.getValue(); + } + return Object.class; + } + private Map parseParameterMapping(String content) { try { return new ParameterExpression(content); diff --git a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java index af32cafc2dc..4bfe091f32c 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java @@ -21,6 +21,7 @@ import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -48,6 +49,7 @@ import org.apache.ibatis.annotations.MapKey; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Options.FlushCachePolicy; +import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Property; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.ResultMap; @@ -81,6 +83,7 @@ import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.parsing.PropertyParser; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.reflection.TypeParameterResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; @@ -175,7 +178,7 @@ private void loadXmlResource() { } } if (inputStream != null) { - XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); + XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type); xmlParser.parse(); } } @@ -295,10 +298,11 @@ private Discriminator applyDiscriminator(String resultMapId, Class resultType void parseStatement(Method method) { final Class parameterTypeClass = getParameterType(method); + final ParamNameResolver paramNameResolver = new ParamNameResolver(configuration, method, type); final LanguageDriver languageDriver = getLanguageDriver(method); getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> { - final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method); + final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, paramNameResolver, languageDriver, method); final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType(); final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null); final String mappedStatementId = type.getName() + "." + method.getName(); @@ -310,7 +314,7 @@ void parseStatement(Method method) { // first check for SelectKey annotation - that overrides everything else SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null); if (selectKey != null) { - keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); + keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), paramNameResolver, languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; @@ -379,7 +383,8 @@ void parseStatement(Method method) { languageDriver, // ResultSets options != null ? nullOrEmpty(options.resultSets()) : null, - statementAnnotation.isDirtySelect()); + statementAnnotation.isDirtySelect(), + paramNameResolver); }); } @@ -394,15 +399,16 @@ private LanguageDriver getLanguageDriver(Method method) { private Class getParameterType(Method method) { Class parameterType = null; - Class[] parameterTypes = method.getParameterTypes(); - for (Class currentParameterType : parameterTypes) { - if (!RowBounds.class.isAssignableFrom(currentParameterType) && !ResultHandler.class.isAssignableFrom(currentParameterType)) { - if (parameterType == null) { - parameterType = currentParameterType; - } else { - // issue #135 - parameterType = ParamMap.class; - } + Parameter[] parameters = method.getParameters(); + for (Parameter param : parameters) { + Class paramType = param.getType(); + if (RowBounds.class.isAssignableFrom(paramType) || ResultHandler.class.isAssignableFrom(paramType)) { + continue; + } + if (parameterType == null && param.getAnnotation(Param.class) == null) { + parameterType = paramType; + } else { + return ParamMap.class; } } return parameterType; @@ -581,7 +587,7 @@ private String nullOrEmpty(String value) { return value == null || value.trim().length() == 0 ? null : value; } - private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class parameterTypeClass, LanguageDriver languageDriver) { + private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class parameterTypeClass, ParamNameResolver paramNameResolver, LanguageDriver languageDriver) { String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX; Class resultTypeClass = selectKeyAnnotation.resultType(); StatementType statementType = selectKeyAnnotation.statementType(); @@ -600,12 +606,12 @@ private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, St ResultSetType resultSetTypeEnum = null; String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId(); - SqlSource sqlSource = buildSqlSource(selectKeyAnnotation, parameterTypeClass, languageDriver, null); + SqlSource sqlSource = buildSqlSourceFromStrings(selectKeyAnnotation.statement(), parameterTypeClass, paramNameResolver, languageDriver); SqlCommandType sqlCommandType = SqlCommandType.SELECT; assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, - keyGenerator, keyProperty, keyColumn, databaseId, languageDriver, null, false); + keyGenerator, keyProperty, keyColumn, databaseId, languageDriver, null, false, paramNameResolver); id = assistant.applyCurrentNamespace(id, false); @@ -615,25 +621,25 @@ private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, St return answer; } - private SqlSource buildSqlSource(Annotation annotation, Class parameterType, LanguageDriver languageDriver, + private SqlSource buildSqlSource(Annotation annotation, Class parameterType, ParamNameResolver paramNameResolver, LanguageDriver languageDriver, Method method) { if (annotation instanceof Select) { - return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof Update) { - return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof Insert) { - return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof Delete) { - return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof SelectKey) { - return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, paramNameResolver, languageDriver); } return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method); } - private SqlSource buildSqlSourceFromStrings(String[] strings, Class parameterTypeClass, + private SqlSource buildSqlSourceFromStrings(String[] strings, Class parameterTypeClass, ParamNameResolver paramNameResolver, LanguageDriver languageDriver) { - return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass); + return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass, paramNameResolver); } @SafeVarargs diff --git a/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java b/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java index 7b8781b26b1..f462a2e6d03 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java @@ -40,7 +40,7 @@ public class ProviderSqlSource implements SqlSource { private final LanguageDriver languageDriver; private final Method mapperMethod; private final Method providerMethod; - private final String[] providerMethodArgumentNames; + private final ParamNameResolver paramNameResolver; private final Class[] providerMethodParameterTypes; private final ProviderContext providerContext; private final Integer providerContextIndex; @@ -130,7 +130,7 @@ public ProviderSqlSource(Configuration configuration, Annotation provider, Class + candidateProviderMethodName + "' not found in SqlProvider '" + this.providerType.getName() + "'."); } this.providerMethod = candidateProviderMethod; - this.providerMethodArgumentNames = new ParamNameResolver(configuration, this.providerMethod).getNames(); + this.paramNameResolver = new ParamNameResolver(configuration, this.providerMethod, mapperType); this.providerMethodParameterTypes = this.providerMethod.getParameterTypes(); ProviderContext candidateProviderContext = null; @@ -168,7 +168,7 @@ private SqlSource createSqlSource(Object parameterObject) { } else { @SuppressWarnings("unchecked") Map params = (Map) parameterObject; - sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames)); + sql = invokeProviderMethod(extractProviderMethodArguments(params, paramNameResolver.getNames())); } } else if (providerMethodParameterTypes.length == 0) { sql = invokeProviderMethod(); @@ -186,7 +186,7 @@ private SqlSource createSqlSource(Object parameterObject) { + "' because SqlProvider method arguments for '" + mapperMethod + "' is an invalid combination."); } Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); - return languageDriver.createSqlSource(configuration, sql, parameterType); + return languageDriver.createSqlSource(configuration, sql, parameterType, paramNameResolver); } catch (BuilderException e) { throw e; } catch (Exception e) { diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java index cbff63e334e..6595f93ef52 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java @@ -59,6 +59,7 @@ public class XMLMapperBuilder extends BaseBuilder { private final MapperBuilderAssistant builderAssistant; private final Map sqlFragments; private final String resource; + private Class mapperClass; @Deprecated public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map sqlFragments, String namespace) { @@ -72,6 +73,11 @@ public XMLMapperBuilder(Reader reader, Configuration configuration, String resou configuration, resource, sqlFragments); } + public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map sqlFragments, Class mapperClass) { + this(inputStream, configuration, resource, sqlFragments, mapperClass.getName()); + this.mapperClass = mapperClass; + } + public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map sqlFragments, String namespace) { this(inputStream, configuration, resource, sqlFragments); this.builderAssistant.setCurrentNamespace(namespace); @@ -133,7 +139,7 @@ private void buildStatementFromContext(List list) { private void buildStatementFromContext(List list, String requiredDatabaseId) { for (XNode context : list) { - final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); + final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId, mapperClass); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java index 82316bb49c9..97d33679d6f 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java @@ -15,9 +15,14 @@ */ package org.apache.ibatis.builder.xml; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; +import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.builder.BaseBuilder; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; @@ -30,6 +35,7 @@ import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; @@ -41,16 +47,22 @@ public class XMLStatementBuilder extends BaseBuilder { private final MapperBuilderAssistant builderAssistant; private final XNode context; private final String requiredDatabaseId; + private final Class mapperClass; public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) { this(configuration, builderAssistant, context, null); } public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) { + this(configuration, builderAssistant, context, null, null); + } + + public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId, Class mapperClass) { super(configuration); this.builderAssistant = builderAssistant; this.context = context; this.requiredDatabaseId = databaseId; + this.mapperClass = mapperClass; } public void parseStatementNode() { @@ -74,6 +86,25 @@ public void parseStatementNode() { String parameterType = context.getStringAttribute("parameterType"); Class parameterTypeClass = resolveClass(parameterType); + ParamNameResolver paramNameResolver = null; + if (parameterTypeClass == null && mapperClass != null) { + List mapperMethods = Arrays.stream(mapperClass.getMethods()) + .filter(m -> m.getName().equals(id) && !m.isDefault() && !m.isBridge()).collect(Collectors.toList()); + if (mapperMethods.size() == 1) { + paramNameResolver = new ParamNameResolver(configuration, mapperMethods.get(0), mapperClass); + if (paramNameResolver.isUseParamMap()) { + parameterTypeClass = ParamMap.class; + } else { + String[] paramNames = paramNameResolver.getNames(); + if (paramNames.length == 1) { + Type paramType = paramNameResolver.getType(paramNames[0]); + if (paramType instanceof Class) { + parameterTypeClass = (Class) paramType; + } + } + } + } + } String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); @@ -93,7 +124,7 @@ public void parseStatementNode() { ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } - SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); + SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass, paramNameResolver); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); @@ -114,7 +145,7 @@ public void parseStatementNode() { builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, - keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect); + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect, paramNameResolver); } private void processSelectKeyNodes(String id, Class parameterTypeClass, LanguageDriver langDriver) { @@ -161,7 +192,7 @@ private void parseSelectKeyNode(String id, XNode nodeToHandle, Class paramete builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, - keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null, false); + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null, false, null); id = builderAssistant.applyCurrentNamespace(id, false); diff --git a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index 22af84642e8..80beceff313 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -17,6 +17,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Parameter; +import java.lang.reflect.Type; import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -64,6 +65,7 @@ import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeException; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; import org.apache.ibatis.util.MapUtil; @@ -102,6 +104,8 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final Map> autoMappingsCache = new HashMap<>(); private final Map> constructorAutoMappingColumns = new HashMap<>(); + private final Map> typeHandlerCache = new HashMap<>(); + // temporary marking flag that indicate using constructor mapping (use field to reduce memory usage) private boolean useConstructorMappings; @@ -357,7 +361,7 @@ private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap r ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { - ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); + ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw, resultMap, null); Object rowValue = getRowValue(rsw, discriminatedResultMap, null); storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } @@ -484,7 +488,7 @@ private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, if (propertyMapping.isCompositeResult() || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) || propertyMapping.getResultSet() != null) { - Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix); + Object value = getPropertyMappingValue(rsw, metaObject, propertyMapping, lazyLoader, columnPrefix); // issue #541 make property optional final String property = propertyMapping.getProperty(); if (property == null) { @@ -505,20 +509,52 @@ private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, return foundValues; } - private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) + private Object getPropertyMappingValue(ResultSetWrapper rsw, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { + final ResultSet rs = rsw.getResultSet(); + final String property = propertyMapping.getProperty(); if (propertyMapping.getNestedQueryId() != null) { return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix); } else if (propertyMapping.getResultSet() != null) { addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK? return DEFERRED; } else { - final TypeHandler typeHandler = propertyMapping.getTypeHandler(); final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); + TypeHandler typeHandler = propertyMapping.getTypeHandler(); + if (typeHandler == null) { + typeHandler = resolvePropertyTypeHandler(rsw, metaResultObject, property, column); + } return typeHandler.getResult(rs, column); } } + private TypeHandler resolvePropertyTypeHandler(ResultSetWrapper rsw, MetaObject metaResultObject, + final String property, final String column) { + CacheKey typeHandlerCacheKey = new CacheKey(); + Class metaResultObjectClass = metaResultObject.getOriginalObject().getClass(); + typeHandlerCacheKey.update(metaResultObjectClass); + typeHandlerCacheKey.update(column); + typeHandlerCacheKey.update(property); + return typeHandlerCache.computeIfAbsent(typeHandlerCacheKey, k -> { + final JdbcType jdbcType = rsw.getJdbcType(column); + final TypeHandler th; + if (property == null) { + th = typeHandlerRegistry.getTypeHandler(jdbcType); + } else { + Type classToHandle = metaResultObject.getGenericSetterType(property).getKey(); + th = configuration.getTypeHandlerResolver().resolve(metaResultObjectClass, + classToHandle, property, jdbcType, null); + if (th == null) { + throw new TypeException("No usable type handler found for mapping the result of column '" + column + + "' to property '" + property + + "'. It was either not specified and/or could not be found for the javaType (" + + classToHandle + ") : jdbcType (" + jdbcType + ") combination."); + } + } + return th; + }); + } + private List createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { final String mapKey = resultMap.getId() + ":" + columnPrefix; List autoMapping = autoMappingsCache.get(mapKey); @@ -546,10 +582,12 @@ private List createAutomaticMappings(ResultSetWrapper if (resultMap.getMappedProperties().contains(property)) { continue; } - final Class propertyType = metaObject.getSetterType(property); - if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) { - final TypeHandler typeHandler = rsw.getTypeHandler(propertyType, columnName); - autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive())); + final Type propertyType = metaObject.getGenericSetterType(property).getKey(); + Class metaObjectClass = metaObject.getOriginalObject().getClass(); + TypeHandler typeHandler = configuration.getTypeHandlerResolver().resolve(metaObjectClass, propertyType, + property, rsw.getJdbcType(columnName), null); + if (typeHandler != null) { + autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType instanceof Class && ((Class)propertyType).isPrimitive())); } else { configuration.getAutoMappingUnknownColumnBehavior() .doAction(mappedStatement, columnName, property, propertyType); @@ -685,7 +723,10 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId()); value = getRowValue(rsw, resultMap, getColumnPrefix(columnPrefix, constructorMapping)); } else { - final TypeHandler typeHandler = constructorMapping.getTypeHandler(); + TypeHandler typeHandler = constructorMapping.getTypeHandler(); + if (typeHandler == null) { + typeHandler = typeHandlerRegistry.getTypeHandler(constructorMapping.getJavaType(), rsw.getJdbcType(column)); + } value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix)); } } catch (ResultMapException | SQLException e) { @@ -931,11 +972,11 @@ private Object instantiateParameterObject(Class parameterType) { // DISCRIMINATOR // - public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException { + public ResultMap resolveDiscriminatedResultMap(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { Set pastDiscriminators = new HashSet<>(); Discriminator discriminator = resultMap.getDiscriminator(); while (discriminator != null) { - final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix); + final Object value = getDiscriminatorValue(rsw, discriminator, columnPrefix); final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value)); if (configuration.hasResultMap(discriminatedMapId)) { resultMap = configuration.getResultMap(discriminatedMapId); @@ -951,10 +992,14 @@ public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap return resultMap; } - private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix) throws SQLException { + private Object getDiscriminatorValue(ResultSetWrapper rsw, Discriminator discriminator, String columnPrefix) throws SQLException { final ResultMapping resultMapping = discriminator.getResultMapping(); - final TypeHandler typeHandler = resultMapping.getTypeHandler(); - return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix)); + String column = prependPrefix(resultMapping.getColumn(), columnPrefix); + TypeHandler typeHandler = resultMapping.getTypeHandler(); + if (typeHandler == null) { + typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.getJavaType(), rsw.getJdbcType(column)); + } + return typeHandler.getResult(rsw.getResultSet(), column); } private String prependPrefix(String columnName, String prefix) { @@ -974,7 +1019,7 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r skipRows(resultSet, rowBounds); Object rowValue = previousRowValue; while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { - final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); + final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw, resultMap, null); final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); Object partialObject = nestedResultObjects.get(rowKey); // issue #577 && #542 @@ -1010,7 +1055,7 @@ private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap result if (nestedResultMapId != null && resultMapping.getResultSet() == null) { try { final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping); - final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix); + final ResultMap nestedResultMap = getNestedResultMap(rsw, nestedResultMapId, columnPrefix); if (resultMapping.getColumnPrefix() == null) { // try to fill circular reference only when columnPrefix // is not specified for the nested result map (issue #215) @@ -1075,9 +1120,9 @@ private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String col return true; } - private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix) throws SQLException { + private ResultMap getNestedResultMap(ResultSetWrapper rsw, String nestedResultMapId, String columnPrefix) throws SQLException { ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId); - return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix); + return resolveDiscriminatedResultMap(rsw, nestedResultMap, columnPrefix); } // @@ -1129,10 +1174,13 @@ private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapp for (ResultMapping resultMapping : resultMappings) { if (resultMapping.isSimple()) { final String column = prependPrefix(resultMapping.getColumn(), columnPrefix); - final TypeHandler th = resultMapping.getTypeHandler(); + TypeHandler th = resultMapping.getTypeHandler(); List mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix); // Issue #114 if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) { + if (th == null) { + th = typeHandlerRegistry.getTypeHandler(rsw.getJdbcType(column)); + } final Object value = th.getResult(rsw.getResultSet(), column); if (value != null || configuration.isReturnInstanceForEmptyRow()) { cacheKey.update(column); diff --git a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java index c5269af470f..539a3fdbf7d 100644 --- a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java +++ b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java @@ -25,6 +25,7 @@ import org.apache.ibatis.executor.keygen.NoKeyGenerator; import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; @@ -56,6 +57,7 @@ public final class MappedStatement { private Log statementLog; private LanguageDriver lang; private String[] resultSets; + private ParamNameResolver paramNameResolver; private boolean dirtySelect; MappedStatement() { @@ -180,6 +182,11 @@ public Builder dirtySelect(boolean dirtySelect) { return this; } + public Builder paramNameResolver(ParamNameResolver paramNameResolver) { + mappedStatement.paramNameResolver = paramNameResolver; + return this; + } + /** * Resul sets. * @@ -300,6 +307,10 @@ public boolean isDirtySelect() { return dirtySelect; } + public ParamNameResolver getParamNameResolver() { + return paramNameResolver; + } + /** * Gets the resul sets. * diff --git a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java index 94abab3bbb4..4cb80d5ca08 100644 --- a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java @@ -100,35 +100,17 @@ public Builder expression(String expression) { } public ParameterMapping build() { - resolveTypeHandler(); validate(); return parameterMapping; } private void validate() { - if (ResultSet.class.equals(parameterMapping.javaType)) { - if (parameterMapping.resultMapId == null) { - throw new IllegalStateException("Missing resultmap in property '" - + parameterMapping.property + "'. " - + "Parameters of type java.sql.ResultSet require a resultmap."); - } - } else { - if (parameterMapping.typeHandler == null) { - throw new IllegalStateException("Type handler was null on parameter mapping for property '" - + parameterMapping.property + "'. It was either not specified and/or could not be found for the javaType (" - + parameterMapping.javaType.getName() + ") : jdbcType (" + parameterMapping.jdbcType + ") combination."); - } + if (ResultSet.class.equals(parameterMapping.javaType) && parameterMapping.resultMapId == null) { + throw new IllegalStateException("Missing resultmap in property '" + + parameterMapping.property + "'. " + + "Parameters of type java.sql.ResultSet require a resultmap."); } } - - private void resolveTypeHandler() { - if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) { - Configuration configuration = parameterMapping.configuration; - TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); - parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType); - } - } - } public String getProperty() { diff --git a/src/main/java/org/apache/ibatis/mapping/ResultMapping.java b/src/main/java/org/apache/ibatis/mapping/ResultMapping.java index 80d5f113968..2bd73dbc0ff 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMapping.java @@ -136,7 +136,6 @@ public ResultMapping build() { // lock down collections resultMapping.flags = Collections.unmodifiableList(resultMapping.flags); resultMapping.composites = Collections.unmodifiableList(resultMapping.composites); - resolveTypeHandler(); validate(); return resultMapping; } @@ -148,7 +147,7 @@ private void validate() { } // Issue #5: there should be no mappings without typehandler if (resultMapping.nestedQueryId == null && resultMapping.nestedResultMapId == null && resultMapping.typeHandler == null) { - throw new IllegalStateException("No typehandler found for property " + resultMapping.property); + // throw new IllegalStateException("No typehandler found for property " + resultMapping.property); } // Issue #4 and GH #39: column is optional only in nested resultmaps but not in the rest if (resultMapping.nestedResultMapId == null && resultMapping.column == null && resultMapping.composites.isEmpty()) { @@ -169,14 +168,6 @@ private void validate() { } } - private void resolveTypeHandler() { - if (resultMapping.typeHandler == null && resultMapping.javaType != null) { - Configuration configuration = resultMapping.configuration; - TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); - resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType); - } - } - public Builder column(String column) { resultMapping.column = column; return this; diff --git a/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java b/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java index 987e721e19a..205d61964a7 100644 --- a/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java +++ b/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java @@ -15,6 +15,7 @@ */ package org.apache.ibatis.reflection; +import java.lang.reflect.Type; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -22,7 +23,7 @@ public class DefaultReflectorFactory implements ReflectorFactory { private boolean classCacheEnabled = true; - private final ConcurrentMap, Reflector> reflectorMap = new ConcurrentHashMap<>(); + private final ConcurrentMap reflectorMap = new ConcurrentHashMap<>(); public DefaultReflectorFactory() { } @@ -38,7 +39,7 @@ public void setClassCacheEnabled(boolean classCacheEnabled) { } @Override - public Reflector findForClass(Class type) { + public Reflector findForClass(Type type) { if (classCacheEnabled) { // synchronized (type) removed see issue #461 return MapUtil.computeIfAbsent(reflectorMap, type, Reflector::new); diff --git a/src/main/java/org/apache/ibatis/reflection/MetaClass.java b/src/main/java/org/apache/ibatis/reflection/MetaClass.java index abbdaadbfa2..134d64a4a14 100644 --- a/src/main/java/org/apache/ibatis/reflection/MetaClass.java +++ b/src/main/java/org/apache/ibatis/reflection/MetaClass.java @@ -15,16 +15,15 @@ */ package org.apache.ibatis.reflection; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.AbstractMap; import java.util.Collection; +import java.util.Map.Entry; -import org.apache.ibatis.reflection.invoker.GetFieldInvoker; import org.apache.ibatis.reflection.invoker.Invoker; -import org.apache.ibatis.reflection.invoker.MethodInvoker; import org.apache.ibatis.reflection.property.PropertyTokenizer; +import org.apache.ibatis.util.MapUtil; /** * @author Clinton Begin @@ -34,12 +33,12 @@ public class MetaClass { private final ReflectorFactory reflectorFactory; private final Reflector reflector; - private MetaClass(Class type, ReflectorFactory reflectorFactory) { + private MetaClass(Type type, ReflectorFactory reflectorFactory) { this.reflectorFactory = reflectorFactory; this.reflector = reflectorFactory.findForClass(type); } - public static MetaClass forClass(Class type, ReflectorFactory reflectorFactory) { + public static MetaClass forClass(Type type, ReflectorFactory reflectorFactory) { return new MetaClass(type, reflectorFactory); } @@ -78,58 +77,57 @@ public Class getSetterType(String name) { } } + public Entry> getGenericSetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaClass metaProp = metaClassForProperty(prop); + return metaProp.getGenericSetterType(prop.getChildren()); + } + return reflector.getGenericSetterType(prop.getName()); + } + public Class getGetterType(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); if (prop.hasNext()) { MetaClass metaProp = metaClassForProperty(prop); return metaProp.getGetterType(prop.getChildren()); } - // issue #506. Resolve the type inside a Collection Object + return getGetterType(prop).getValue(); + } + + public Entry> getGenericGetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaClass metaProp = metaClassForProperty(prop); + return metaProp.getGenericGetterType(prop.getChildren()); + } return getGetterType(prop); } private MetaClass metaClassForProperty(PropertyTokenizer prop) { - Class propType = getGetterType(prop); + Class propType = getGetterType(prop).getValue(); return MetaClass.forClass(propType, reflectorFactory); } - private Class getGetterType(PropertyTokenizer prop) { - Class type = reflector.getGetterType(prop.getName()); - if (prop.getIndex() != null && Collection.class.isAssignableFrom(type)) { - Type returnType = getGenericGetterType(prop.getName()); + private Entry> getGetterType(PropertyTokenizer prop) { + // Resolve the type inside a Collection Object + // https://github.com/mybatis/old-google-code-issues/issues/506 + Entry> pair = reflector.getGenericGetterType(prop.getName()); + if (prop.getIndex() != null && Collection.class.isAssignableFrom(pair.getValue())) { + Type returnType = pair.getKey(); if (returnType instanceof ParameterizedType) { Type[] actualTypeArguments = ((ParameterizedType) returnType).getActualTypeArguments(); if (actualTypeArguments != null && actualTypeArguments.length == 1) { returnType = actualTypeArguments[0]; if (returnType instanceof Class) { - type = (Class) returnType; + return MapUtil.entry(returnType, (Class)returnType); } else if (returnType instanceof ParameterizedType) { - type = (Class) ((ParameterizedType) returnType).getRawType(); + return MapUtil.entry(returnType, (Class)((ParameterizedType)returnType).getRawType()); } } } } - return type; - } - - private Type getGenericGetterType(String propertyName) { - try { - Invoker invoker = reflector.getGetInvoker(propertyName); - if (invoker instanceof MethodInvoker) { - Field declaredMethod = MethodInvoker.class.getDeclaredField("method"); - declaredMethod.setAccessible(true); - Method method = (Method) declaredMethod.get(invoker); - return TypeParameterResolver.resolveReturnType(method, reflector.getType()); - } else if (invoker instanceof GetFieldInvoker) { - Field declaredField = GetFieldInvoker.class.getDeclaredField("field"); - declaredField.setAccessible(true); - Field field = (Field) declaredField.get(invoker); - return TypeParameterResolver.resolveFieldType(field, reflector.getType()); - } - } catch (NoSuchFieldException | IllegalAccessException e) { - // Ignored - } - return null; + return pair; } public boolean hasSetter(String name) { diff --git a/src/main/java/org/apache/ibatis/reflection/MetaObject.java b/src/main/java/org/apache/ibatis/reflection/MetaObject.java index e95dd527d09..1a765fb1022 100644 --- a/src/main/java/org/apache/ibatis/reflection/MetaObject.java +++ b/src/main/java/org/apache/ibatis/reflection/MetaObject.java @@ -15,9 +15,11 @@ */ package org.apache.ibatis.reflection; +import java.lang.reflect.Type; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.reflection.property.PropertyTokenizer; @@ -97,10 +99,18 @@ public Class getSetterType(String name) { return objectWrapper.getSetterType(name); } + public Entry> getGenericSetterType(String name) { + return objectWrapper.getGenericSetterType(name); + } + public Class getGetterType(String name) { return objectWrapper.getGetterType(name); } + public Entry> getGenericGetterType(String name) { + return objectWrapper.getGenericGetterType(name); + } + public boolean hasSetter(String name) { return objectWrapper.hasSetter(name); } diff --git a/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java b/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java index e47c51ca218..1f2eff830bf 100644 --- a/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java +++ b/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java @@ -16,9 +16,13 @@ package org.apache.ibatis.reflection; import java.lang.annotation.Annotation; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -27,6 +31,7 @@ import org.apache.ibatis.annotations.Param; import org.apache.ibatis.binding.MapperMethod.ParamMap; +import org.apache.ibatis.reflection.property.PropertyTokenizer; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; @@ -51,14 +56,17 @@ public class ParamNameResolver { * */ private final SortedMap names; + private final Map typeMap = new HashMap<>(); private boolean hasParamAnnotation; + private boolean useParamMap; - public ParamNameResolver(Configuration config, Method method) { + public ParamNameResolver(Configuration config, Method method, Class mapperClass) { this.useActualParamName = config.isUseActualParamName(); final Class[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap map = new TreeMap<>(); + Type[] actualParamTypes = TypeParameterResolver.resolveParamTypes(method, mapperClass); int paramCount = paramAnnotations.length; // get names from @Param annotations for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { @@ -70,6 +78,7 @@ public ParamNameResolver(Configuration config, Method method) { for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true; + useParamMap = true; name = ((Param) annotation).value(); break; } @@ -86,8 +95,31 @@ public ParamNameResolver(Configuration config, Method method) { } } map.put(paramIndex, name); + typeMap.put(name, actualParamTypes[paramIndex]); } names = Collections.unmodifiableSortedMap(map); + if (names.size() > 1) { + useParamMap = true; + } + if (names.size() == 1) { + Type soleParamType = actualParamTypes[0]; + if (soleParamType instanceof GenericArrayType) { + typeMap.put("array", soleParamType); + } else { + Class soleParamClass = null; + if (soleParamType instanceof ParameterizedType) { + soleParamClass = (Class) ((ParameterizedType) soleParamType).getRawType(); + } else if (soleParamType instanceof Class) { + soleParamClass = (Class) soleParamType; + } + if (Collection.class.isAssignableFrom(soleParamClass)) { + typeMap.put("collection", soleParamType); + if (List.class.isAssignableFrom(soleParamClass)) { + typeMap.put("list", soleParamType); + } + } + } + } } private String getActualParamName(Method method, int paramIndex) { @@ -143,6 +175,21 @@ public Object getNamedParams(Object[] args) { } } + public Type getType(String name) { + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(name); + Type type = typeMap.get(propertyTokenizer.getName()); + if (propertyTokenizer.getIndex() != null) { + if (type instanceof ParameterizedType) { + Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments(); + return typeArgs[0]; + } else if (type instanceof Class && ((Class)type).isArray()) { + return ((Class)type).getComponentType(); + } + } + // TODO: param1, param2 + return type; + } + /** * Wrap to a {@link ParamMap} if object is {@link Collection} or array. * @@ -170,4 +217,7 @@ public static Object wrapToMapIfCollection(Object object, String actualParamName return object; } + public boolean isUseParamMap() { + return useParamMap; + } } diff --git a/src/main/java/org/apache/ibatis/reflection/Reflector.java b/src/main/java/org/apache/ibatis/reflection/Reflector.java index a326b70bb12..f6801f51588 100644 --- a/src/main/java/org/apache/ibatis/reflection/Reflector.java +++ b/src/main/java/org/apache/ibatis/reflection/Reflector.java @@ -54,22 +54,30 @@ public class Reflector { private static final MethodHandle isRecordMethodHandle = getIsRecordMethodHandle(); - private final Class type; + private final Type type; + private final Class clazz; private final String[] readablePropertyNames; private final String[] writablePropertyNames; private final Map setMethods = new HashMap<>(); private final Map getMethods = new HashMap<>(); - private final Map> setTypes = new HashMap<>(); - private final Map> getTypes = new HashMap<>(); + private final Map>> setTypes = new HashMap<>(); + private final Map>> getTypes = new HashMap<>(); private Constructor defaultConstructor; private Map caseInsensitivePropertyMap = new HashMap<>(); - public Reflector(Class clazz) { - type = clazz; + private static final Entry> nullEntry = MapUtil.entry(null, null); + + public Reflector(Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + this.clazz = (Class) ((ParameterizedType)type).getRawType(); + } else { + this.clazz = (Class)type; + } addDefaultConstructor(clazz); Method[] classMethods = getClassMethods(clazz); - if (isRecord(type)) { + if (isRecord(clazz)) { addRecordGetMethods(classMethods); } else { addGetMethods(classMethods); @@ -144,7 +152,7 @@ private void addGetMethod(String name, Method method, boolean isAmbiguous) { : new MethodInvoker(method); getMethods.put(name, invoker); Type returnType = TypeParameterResolver.resolveReturnType(method, type); - getTypes.put(name, typeToClass(returnType)); + getTypes.put(name, MapUtil.entry(returnType, typeToClass(returnType))); } private void addSetMethods(Method[] methods) { @@ -165,7 +173,7 @@ private void resolveSetterConflicts(Map> conflictingSetters for (Entry> entry : conflictingSetters.entrySet()) { String propName = entry.getKey(); List setters = entry.getValue(); - Class getterType = getTypes.get(propName); + Class getterType = getTypes.getOrDefault(propName, nullEntry).getValue(); boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker; boolean isSetterAmbiguous = false; Method match = null; @@ -203,7 +211,7 @@ private Method pickBetterSetter(Method setter1, Method setter2, String property) property, setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName())); setMethods.put(property, invoker); Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type); - setTypes.put(property, typeToClass(paramTypes[0])); + setTypes.put(property, MapUtil.entry(paramTypes[0], typeToClass(paramTypes[0]))); return null; } @@ -211,28 +219,24 @@ private void addSetMethod(String name, Method method) { MethodInvoker invoker = new MethodInvoker(method); setMethods.put(name, invoker); Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type); - setTypes.put(name, typeToClass(paramTypes[0])); + setTypes.put(name, MapUtil.entry(paramTypes[0], typeToClass(paramTypes[0]))); } private Class typeToClass(Type src) { - Class result = null; if (src instanceof Class) { - result = (Class) src; + return (Class) src; } else if (src instanceof ParameterizedType) { - result = (Class) ((ParameterizedType) src).getRawType(); + return (Class) ((ParameterizedType) src).getRawType(); } else if (src instanceof GenericArrayType) { Type componentType = ((GenericArrayType) src).getGenericComponentType(); if (componentType instanceof Class) { - result = Array.newInstance((Class) componentType, 0).getClass(); + return Array.newInstance((Class) componentType, 0).getClass(); } else { Class componentClass = typeToClass(componentType); - result = Array.newInstance(componentClass, 0).getClass(); + return Array.newInstance(componentClass, 0).getClass(); } } - if (result == null) { - result = Object.class; - } - return result; + return Object.class; } private void addFields(Class clazz) { @@ -260,7 +264,7 @@ private void addSetField(Field field) { if (isValidPropertyName(field.getName())) { setMethods.put(field.getName(), new SetFieldInvoker(field)); Type fieldType = TypeParameterResolver.resolveFieldType(field, type); - setTypes.put(field.getName(), typeToClass(fieldType)); + setTypes.put(field.getName(), MapUtil.entry(fieldType, typeToClass(fieldType))); } } @@ -268,7 +272,7 @@ private void addGetField(Field field) { if (isValidPropertyName(field.getName())) { getMethods.put(field.getName(), new GetFieldInvoker(field)); Type fieldType = TypeParameterResolver.resolveFieldType(field, type); - getTypes.put(field.getName(), typeToClass(fieldType)); + getTypes.put(field.getName(), MapUtil.entry(fieldType, typeToClass(fieldType))); } } @@ -358,14 +362,14 @@ public static boolean canControlMemberAccessible() { * @return The class name */ public Class getType() { - return type; + return clazz; } public Constructor getDefaultConstructor() { if (defaultConstructor != null) { return defaultConstructor; } else { - throw new ReflectionException("There is no default constructor for " + type); + throw new ReflectionException("There is no default constructor for " + clazz); } } @@ -376,7 +380,7 @@ public boolean hasDefaultConstructor() { public Invoker getSetInvoker(String propertyName) { Invoker method = setMethods.get(propertyName); if (method == null) { - throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + clazz + "'"); } return method; } @@ -384,7 +388,7 @@ public Invoker getSetInvoker(String propertyName) { public Invoker getGetInvoker(String propertyName) { Invoker method = getMethods.get(propertyName); if (method == null) { - throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + clazz + "'"); } return method; } @@ -396,13 +400,19 @@ public Invoker getGetInvoker(String propertyName) { * @return The Class of the property setter */ public Class getSetterType(String propertyName) { - Class clazz = setTypes.get(propertyName); + Class clazz = setTypes.get(propertyName).getValue(); if (clazz == null) { - throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + clazz + "'"); } return clazz; } + public Entry> getGenericSetterType(String propertyName) { + return setTypes.computeIfAbsent(propertyName, k -> { + throw new ReflectionException("There is no setter for property named '" + k + "' in '" + clazz + "'"); + }); + } + /** * Gets the type for a property getter. * @@ -410,13 +420,19 @@ public Class getSetterType(String propertyName) { * @return The Class of the property getter */ public Class getGetterType(String propertyName) { - Class clazz = getTypes.get(propertyName); + Class clazz = getTypes.getOrDefault(propertyName, nullEntry).getValue(); if (clazz == null) { - throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + clazz + "'"); } return clazz; } + public Entry> getGenericGetterType(String propertyName) { + return getTypes.computeIfAbsent(propertyName, k -> { + throw new ReflectionException("There is no getter for property named '" + k + "' in '" + clazz + "'"); + }); + } + /** * Gets an array of the readable properties for an object. * diff --git a/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java b/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java index 33e4b108f1c..f50b4b440d2 100644 --- a/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java +++ b/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java @@ -15,11 +15,13 @@ */ package org.apache.ibatis.reflection; +import java.lang.reflect.Type; + public interface ReflectorFactory { boolean isClassCacheEnabled(); void setClassCacheEnabled(boolean classCacheEnabled); - Reflector findForClass(Class type); -} \ No newline at end of file + Reflector findForClass(Type type); +} diff --git a/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java b/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java index 9f2df476c93..5b866d4a4ba 100644 --- a/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java +++ b/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java @@ -24,12 +24,22 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.Arrays; +import java.util.Objects; /** * @author Iwao AVE! */ public class TypeParameterResolver { + public static Type[] resolveClassTypeParams(Class classWithTypeParams, Class childClass) { + TypeVariable[] typeArgs = classWithTypeParams.getTypeParameters(); + Type[] result = new Type[typeArgs.length]; + for (int i = 0; i < typeArgs.length; i++) { + result[i] = resolveTypeVar(typeArgs[i], childClass, classWithTypeParams); + } + return result; + } + /** * Resolve field type. * @@ -160,6 +170,14 @@ private static Type resolveTypeVar(TypeVariable typeVar, Type srcType, Class< } else if (srcType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) srcType; clazz = (Class) parameterizedType.getRawType(); + if (clazz == declaringClass) { + TypeVariable[] typeVars = declaringClass.getTypeParameters(); + for (int i=0 ; i < typeVars.length; i++) { + if (typeVar.equals(typeVars[i])) { + return parameterizedType.getActualTypeArguments()[i]; + } + } + } } else { throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass()); } @@ -266,6 +284,21 @@ public Type getRawType() { return rawType; } + @Override + public int hashCode() { + return (ownerType == null ? 0 : ownerType.hashCode()) ^ Arrays.hashCode(actualTypeArguments) ^ rawType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ParameterizedType)) { + return false; + } + ParameterizedType other = (ParameterizedType) obj; + return rawType.equals(other.getRawType()) && Objects.equals(ownerType, other.getOwnerType()) + && Arrays.equals(actualTypeArguments, other.getActualTypeArguments()); + } + @Override public String toString() { return "ParameterizedTypeImpl [rawType=" + rawType + ", ownerType=" + ownerType + ", actualTypeArguments=" + Arrays.toString(actualTypeArguments) + "]"; diff --git a/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java b/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java index cae43e0f0e4..421a7ed9579 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java @@ -15,7 +15,9 @@ */ package org.apache.ibatis.reflection.wrapper; +import java.lang.reflect.Type; import java.util.List; +import java.util.Map.Entry; import org.apache.ibatis.reflection.ExceptionUtil; import org.apache.ibatis.reflection.MetaClass; @@ -90,6 +92,21 @@ public Class getSetterType(String name) { } } + @Override + public Entry> getGenericSetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaObject metaValue = metaObject.metaObjectForProperty(prop.getIndexedName()); + if (metaValue == SystemMetaObject.NULL_META_OBJECT) { + return metaClass.getGenericSetterType(name); + } else { + return metaValue.getGenericSetterType(prop.getChildren()); + } + } else { + return metaClass.getGenericSetterType(name); + } + } + @Override public Class getGetterType(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); @@ -105,6 +122,21 @@ public Class getGetterType(String name) { } } + @Override + public Entry> getGenericGetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaObject metaValue = metaObject.metaObjectForProperty(prop.getIndexedName()); + if (metaValue == SystemMetaObject.NULL_META_OBJECT) { + return metaClass.getGenericGetterType(name); + } else { + return metaValue.getGenericGetterType(prop.getChildren()); + } + } else { + return metaClass.getGenericGetterType(name); + } + } + @Override public boolean hasSetter(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); diff --git a/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java b/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java index c59b42f6b44..3cd0426ef11 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java @@ -15,14 +15,18 @@ */ package org.apache.ibatis.reflection.wrapper; +import java.lang.reflect.Type; +import java.util.AbstractMap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.reflection.property.PropertyTokenizer; +import org.apache.ibatis.util.MapUtil; /** * @author Clinton Begin @@ -90,6 +94,12 @@ public Class getSetterType(String name) { } } + @Override + public Entry> getGenericSetterType(String name) { + Class setterType = getSetterType(name); + return MapUtil.entry(setterType, setterType); + } + @Override public Class getGetterType(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); @@ -109,6 +119,12 @@ public Class getGetterType(String name) { } } + @Override + public Entry> getGenericGetterType(String name) { + Class getterType = getGetterType(name); + return MapUtil.entry(getterType, getterType); + } + @Override public boolean hasSetter(String name) { return true; diff --git a/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java b/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java index 295abdd6e7a..3d2b18a92aa 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java @@ -15,7 +15,9 @@ */ package org.apache.ibatis.reflection.wrapper; +import java.lang.reflect.Type; import java.util.List; +import java.util.Map.Entry; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.factory.ObjectFactory; @@ -40,6 +42,14 @@ public interface ObjectWrapper { Class getGetterType(String name); + default Entry> getGenericSetterType(String name) { + throw new UnsupportedOperationException("'" + this.getClass() + "' must override the default method 'getGenericSetterType()'."); + } + + default Entry> getGenericGetterType(String name) { + throw new UnsupportedOperationException("'" + this.getClass() + "' must override the default method 'getGenericGetterType()'."); + } + boolean hasSetter(String name); boolean hasGetter(String name); diff --git a/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java b/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java index ed681587cc8..2c93f72bec5 100644 --- a/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java @@ -20,6 +20,7 @@ import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.session.Configuration; @@ -48,6 +49,10 @@ public interface LanguageDriver { */ SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType); + default SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType, ParamNameResolver paramNameResolver) { + return createSqlSource(configuration, script, parameterType); + } + /** * Creates an {@link SqlSource} that will hold the statement read from an annotation. * It is called during startup, when the mapped statement is read from a class or an xml file. @@ -59,4 +64,8 @@ public interface LanguageDriver { */ SqlSource createSqlSource(Configuration configuration, String script, Class parameterType); + default SqlSource createSqlSource(Configuration configuration, String script, Class parameterType, ParamNameResolver paramNameResolver) { + return createSqlSource(configuration, script, parameterType); + } + } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java index a9f5a91a0b8..93eb520aeef 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java @@ -15,19 +15,31 @@ */ package org.apache.ibatis.scripting.defaults; +import java.lang.reflect.Type; +import java.sql.CallableStatement; +import java.sql.ParameterMetaData; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; +import java.util.HashMap; import java.util.List; +import java.util.Map.Entry; +import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.executor.ErrorContext; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.reflection.MetaClass; import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.ParamNameResolver; +import org.apache.ibatis.reflection.property.PropertyTokenizer; import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.ObjectTypeHandler; import org.apache.ibatis.type.TypeException; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; @@ -45,6 +57,26 @@ public class DefaultParameterHandler implements ParameterHandler { private final BoundSql boundSql; private final Configuration configuration; + private ParameterMetaData paramMetaData; + private MetaObject paramMetaObject; + private HashMap, MetaClass> metaClassCache = new HashMap<>(); + private static final TypeHandler nullTypeHandler = new ObjectTypeHandler(); + private static final ParameterMetaData nullParameterMetaData = new ParameterMetaData() { + // @formatter:off + public T unwrap(Class iface) throws SQLException { return null; } + public boolean isWrapperFor(Class iface) throws SQLException { return false; } + public boolean isSigned(int param) throws SQLException { return false; } + public int isNullable(int param) throws SQLException { return 0; } + public int getScale(int param) throws SQLException { return 0; } + public int getPrecision(int param) throws SQLException { return 0; } + public String getParameterTypeName(int param) throws SQLException { return null; } + public int getParameterType(int param) throws SQLException { return 0; } + public int getParameterMode(int param) throws SQLException { return 0; } + public int getParameterCount() throws SQLException { return 0; } + public String getParameterClassName(int param) throws SQLException { return null; } + // @formatter:on + }; + public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.mappedStatement = mappedStatement; this.configuration = mappedStatement.getConfiguration(); @@ -58,30 +90,83 @@ public Object getParameterObject() { return parameterObject; } + @SuppressWarnings("rawtypes") @Override public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { + ParamNameResolver paramNameResolver = mappedStatement.getParamNameResolver(); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); + JdbcType jdbcType = parameterMapping.getJdbcType(); + JdbcType actualJdbcType = jdbcType == null ? getParamJdbcType(ps, i + 1) : jdbcType; + Type propertyGenericType = null; + TypeHandler typeHandler = parameterMapping.getTypeHandler(); if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName); + if (typeHandler == null) { + typeHandler = configuration.getTypeHandlerResolver().resolve(value.getClass(), null, null, actualJdbcType, + null); + } } else if (parameterObject == null) { value = null; - } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { - value = parameterObject; } else { - MetaObject metaObject = configuration.newMetaObject(parameterObject); - value = metaObject.getValue(propertyName); + Class parameterClass = parameterObject.getClass(); + TypeHandler paramTypeHandler = typeHandlerRegistry.getTypeHandler(parameterClass, actualJdbcType); + if (paramTypeHandler != null) { + value = parameterObject; + typeHandler = paramTypeHandler; + } else { + MetaObject metaObject = getParamMetaObject(); + value = metaObject.getValue(propertyName); + if (typeHandler == null && value != null) { + if (paramNameResolver != null && ParamMap.class.equals(parameterClass)) { + Type actualParamType = paramNameResolver.getType(propertyName); + if (actualParamType instanceof Class) { + Class actualParamClass = (Class) actualParamType; + MetaClass metaClass = metaClassCache.computeIfAbsent(actualParamClass, + k -> MetaClass.forClass(k, configuration.getReflectorFactory())); + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(propertyName); + String multiParamsPropertyName; + if (propertyTokenizer.hasNext()) { + multiParamsPropertyName = propertyTokenizer.getChildren(); + if (metaClass.hasGetter(multiParamsPropertyName)) { + Entry> getterType = metaClass.getGenericGetterType(multiParamsPropertyName); + propertyGenericType = getterType.getKey(); + } + } else { + propertyGenericType = actualParamClass; + } + } + } else { + try { + propertyGenericType = metaObject.getGenericGetterType(propertyName).getKey(); + typeHandler = configuration.getTypeHandlerResolver().resolve(parameterClass, propertyGenericType, + propertyName, actualJdbcType, null); + } catch (Exception e) { + // Not always resolvable + } + } + } + } } - TypeHandler typeHandler = parameterMapping.getTypeHandler(); - JdbcType jdbcType = parameterMapping.getJdbcType(); - if (value == null && jdbcType == null) { - jdbcType = configuration.getJdbcTypeForNull(); + if (value == null) { + if (jdbcType == null) { + jdbcType = configuration.getJdbcTypeForNull(); + } + if (typeHandler == null) { + typeHandler = nullTypeHandler; + } + } else if (typeHandler == null) { + if (propertyGenericType == null) { + propertyGenericType = value.getClass(); + } + typeHandler = configuration.getTypeHandlerResolver().resolve(parameterObject.getClass(), + propertyGenericType, propertyName, actualJdbcType, null); } try { typeHandler.setParameter(ps, i + 1, value, jdbcType); @@ -93,4 +178,27 @@ public void setParameters(PreparedStatement ps) { } } + private MetaObject getParamMetaObject() { + if (paramMetaObject != null) { + return paramMetaObject; + } + paramMetaObject = configuration.newMetaObject(parameterObject); + return paramMetaObject; + } + + private JdbcType getParamJdbcType(PreparedStatement ps, int paramIndex) { + try { + if (paramMetaData == null) { + try { + paramMetaData = ps.getParameterMetaData(); + } catch (SQLException e) { + paramMetaData = nullParameterMetaData; + } + } + return JdbcType.forCode(paramMetaData.getParameterType(paramIndex)); + } catch (SQLException e) { + return null; + } + } + } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java b/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java index 731b364517b..043e4e0d062 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java @@ -18,6 +18,7 @@ import org.apache.ibatis.builder.BuilderException; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; import org.apache.ibatis.session.Configuration; @@ -39,8 +40,8 @@ public SqlSource createSqlSource(Configuration configuration, XNode script, Clas } @Override - public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) { - SqlSource source = super.createSqlSource(configuration, script, parameterType); + public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType, ParamNameResolver paramNameResolver) { + SqlSource source = super.createSqlSource(configuration, script, parameterType, paramNameResolver); checkIsNotDynamic(source); return source; } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java index 556712e5501..ad54df5d301 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java @@ -20,6 +20,7 @@ import org.apache.ibatis.builder.SqlSourceBuilder; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.xmltags.DynamicContext; import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; import org.apache.ibatis.scripting.xmltags.SqlNode; @@ -37,13 +38,21 @@ public class RawSqlSource implements SqlSource { private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType) { - this(configuration, getSql(configuration, rootSqlNode), parameterType); + this(configuration, rootSqlNode, parameterType, null); + } + + public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType, ParamNameResolver paramNameResolver) { + this(configuration, getSql(configuration, rootSqlNode), parameterType, paramNameResolver); } public RawSqlSource(Configuration configuration, String sql, Class parameterType) { + this(configuration, sql, parameterType, null); + } + + public RawSqlSource(Configuration configuration, String sql, Class parameterType, ParamNameResolver paramNameResolver) { SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class clazz = parameterType == null ? Object.class : parameterType; - sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); + sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>(), paramNameResolver); } private static String getSql(Configuration configuration, SqlNode rootSqlNode) { diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java index fce0bfa5396..1e6edf6d87f 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java @@ -18,6 +18,7 @@ import org.apache.ibatis.builder.SqlSourceBuilder; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.session.Configuration; /** @@ -27,10 +28,16 @@ public class DynamicSqlSource implements SqlSource { private final Configuration configuration; private final SqlNode rootSqlNode; + private final ParamNameResolver paramNameResolver; public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { + this(configuration, rootSqlNode, null); + } + + public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode, ParamNameResolver paramNameResolver) { this.configuration = configuration; this.rootSqlNode = rootSqlNode; + this.paramNameResolver = paramNameResolver; } @Override @@ -39,7 +46,7 @@ public BoundSql getBoundSql(Object parameterObject) { rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); - SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings(), paramNameResolver); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java b/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java index 8a1521d59a6..aaf132e8618 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java @@ -23,6 +23,7 @@ import org.apache.ibatis.parsing.PropertyParser; import org.apache.ibatis.parsing.XNode; import org.apache.ibatis.parsing.XPathParser; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.scripting.defaults.RawSqlSource; @@ -40,12 +41,22 @@ public ParameterHandler createParameterHandler(MappedStatement mappedStatement, @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType) { - XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); + return createSqlSource(configuration, script, parameterType, null); + } + + @Override + public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType, ParamNameResolver paramNameResolver) { + XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType, paramNameResolver); return builder.parseScriptNode(); } @Override public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) { + return createSqlSource(configuration, script, parameterType, null); + } + + @Override + public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType, ParamNameResolver paramNameResolver) { // issue #3 if (script.startsWith("