diff --git a/src/main/java/org/mybatis/scripting/velocity/InDirective.java b/src/main/java/org/mybatis/scripting/velocity/InDirective.java new file mode 100644 index 0000000..615d5a5 --- /dev/null +++ b/src/main/java/org/mybatis/scripting/velocity/InDirective.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012 MyBatis.org. + * + * 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 + * + * http://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.mybatis.scripting.velocity; + +import java.io.IOException; +import java.io.Writer; +import java.util.Iterator; + +import org.apache.velocity.context.InternalContextAdapter; +import org.apache.velocity.exception.MethodInvocationException; +import org.apache.velocity.exception.ParseErrorException; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.exception.TemplateInitException; +import org.apache.velocity.exception.VelocityException; +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.directive.StopCommand; +import org.apache.velocity.runtime.parser.ParserTreeConstants; +import org.apache.velocity.runtime.parser.node.ASTReference; +import org.apache.velocity.runtime.parser.node.ASTStringLiteral; +import org.apache.velocity.runtime.parser.node.Node; +import org.apache.velocity.util.introspection.Info; + +/** + * #in($collection $item COLUMN). + * + */ +public class InDirective extends RepeatDirective { + + /** Immutable fields */ + private String var; + private String open = "("; + private String close = ")"; + private String separator = ", "; + private String column = ""; + + @Override + public String getName() { + return "in"; + } + + @Override + public void init(RuntimeServices rs, InternalContextAdapter context, Node node) throws TemplateInitException { + super.init(rs, context, node); + final int n = node.jjtGetNumChildren() - 1; + for (int i = 1; i < n; i++) { + Node child = node.jjtGetChild(i); + if (i == 1) { + if (child.getType() == ParserTreeConstants.JJTREFERENCE) { + var = ((ASTReference) child).getRootString(); + } else { + throw new TemplateInitException("Syntax error", getTemplateName(), getLine(), getColumn()); + } + } else if (child.getType() == ParserTreeConstants.JJTSTRINGLITERAL) { + String value = (String) ((ASTStringLiteral)child).value(context); + switch (i) { + case 2: + this.column = value; + break; + } + } else { + throw new TemplateInitException("Syntax error", getTemplateName(), getLine(), getColumn()); + } + } + uberInfo = new Info(this.getTemplateName(), getLine(), getColumn()); + } + + @Override + public boolean render(InternalContextAdapter context, Writer writer, Node node) throws IOException, ResourceNotFoundException, + ParseErrorException, MethodInvocationException { + Object listObject = node.jjtGetChild(0).value(context); + if (listObject == null) { + return false; + } + + Iterator iterator = null; + + try { + iterator = this.rsvc.getUberspect().getIterator(listObject, uberInfo); + } catch (RuntimeException e) { + throw e; + } catch (Exception ee) { + String msg = "Error getting iterator for #in at " + uberInfo; + rsvc.getLog().error(msg, ee); + throw new VelocityException(msg, ee); + } + + if (iterator == null) { + throw new VelocityException("Invalid collection"); + } + + int counter = 0; + Object o = context.get(this.var); + + ParameterMappingCollector collector = (ParameterMappingCollector) context.get(SQLScriptSource.MAPPING_COLLECTOR_KEY); + String savedItemKey = collector.getItemKey(); + collector.setItemKey(this.var); + RepeatScope foreach = new RepeatScope(this, context.get(getName()), var); + context.put(getName(), foreach); + + NullHolderContext nullHolderContext = null; + Object value = null; + writer.append(this.open); + while (iterator.hasNext()) { + + if(counter % MAX_IN_CLAUSE_SIZE == 0) { + writer.append(this.open); // Group begins + writer.append(this.column); + writer.append(" IN "); + writer.append(this.open); // In starts + } + + value = iterator.next(); + put(context, this.var, value); + foreach.index++; + foreach.hasNext = iterator.hasNext(); + + try { + if (value == null) { + if (nullHolderContext == null) { + nullHolderContext = new NullHolderContext(this.var, context); + } + node.jjtGetChild(node.jjtGetNumChildren() - 1).render(nullHolderContext, writer); + } else { + node.jjtGetChild(node.jjtGetNumChildren() - 1).render(context, writer); + } + } catch (StopCommand stop) { + if (stop.isFor(this)) { + break; + } else { + clean(context, o, collector, savedItemKey); + throw stop; + } + } + counter++; + + + if((counter > 0 && counter % MAX_IN_CLAUSE_SIZE == 0) || !iterator.hasNext()) { + writer.append(this.close); // In ends + writer.append(this.close); // Group ends + if(iterator.hasNext()) { + writer.append(" OR "); + } + } else if(iterator.hasNext()) { + writer.append(this.separator); + } + + } + + writer.append(this.close); + clean(context, o, collector, savedItemKey); + return true; + } + + @Override + public int getType() { + return BLOCK; + } + +} diff --git a/src/main/java/org/mybatis/scripting/velocity/RepeatDirective.java b/src/main/java/org/mybatis/scripting/velocity/RepeatDirective.java index 589d572..1f5f671 100644 --- a/src/main/java/org/mybatis/scripting/velocity/RepeatDirective.java +++ b/src/main/java/org/mybatis/scripting/velocity/RepeatDirective.java @@ -40,6 +40,8 @@ */ public class RepeatDirective extends Directive { + protected static final int MAX_IN_CLAUSE_SIZE = 1000; + /** Immutable fields */ private String var; private String open = ""; @@ -149,7 +151,7 @@ public boolean render(InternalContextAdapter context, Writer writer, Node node) } counter++; - maxNbrLoopsExceeded = counter >= 1000; + maxNbrLoopsExceeded = counter >= MAX_IN_CLAUSE_SIZE; if (i.hasNext() && !maxNbrLoopsExceeded) { writer.append(separator); @@ -236,7 +238,7 @@ protected static class NullHolderContext extends ChainedInternalContextAdapter { private String loopVariableKey = ""; private boolean active = true; - private NullHolderContext(String key, InternalContextAdapter context) { + protected NullHolderContext(String key, InternalContextAdapter context) { super(context); if (key != null) { loopVariableKey = key; diff --git a/src/main/java/org/mybatis/scripting/velocity/VelocityFacade.java b/src/main/java/org/mybatis/scripting/velocity/VelocityFacade.java index edfb7eb..0e65860 100644 --- a/src/main/java/org/mybatis/scripting/velocity/VelocityFacade.java +++ b/src/main/java/org/mybatis/scripting/velocity/VelocityFacade.java @@ -36,6 +36,7 @@ public class VelocityFacade { private static final String DIRECTIVES = TrimDirective.class.getName() + "," + WhereDirective.class.getName() + "," + SetDirective.class.getName() + + "," + InDirective.class.getName() + "," + RepeatDirective.class.getName(); private static final RuntimeInstance engine; diff --git a/src/test/java/org/mybatis/scripting/velocity/InDirectiveTest.java b/src/test/java/org/mybatis/scripting/velocity/InDirectiveTest.java new file mode 100644 index 0000000..928ce83 --- /dev/null +++ b/src/test/java/org/mybatis/scripting/velocity/InDirectiveTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012 MyBatis.org. + * + * 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 + * + * http://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.mybatis.scripting.velocity; + +import static org.junit.Assert.assertEquals; + +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Properties; + +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.session.Configuration; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.junit.BeforeClass; +import org.junit.Test; + +public class InDirectiveTest { + + static VelocityContext ctxt; + static VelocityEngine velocity; + + @BeforeClass + public static void setUpClass() throws Exception { + Properties p = new Properties(); + p.setProperty("userdirective", InDirective.class.getName()); + velocity = new VelocityEngine(); + velocity.init(p); + ctxt = new VelocityContext(); + ctxt.put(SQLScriptSource.MAPPING_COLLECTOR_KEY, + new ParameterMappingCollector(new ParameterMapping[]{}, new HashMap(), new Configuration())); + StringWriter writer = new StringWriter(); + velocity.evaluate(ctxt, writer, "WARM", "1+1"); + } + + @Test + public void ensureInClauseHasOne() throws Exception { + StringWriter w = new StringWriter(); + ctxt.put("list", Collections.singletonList("?")); + velocity.evaluate(ctxt, w, "TEST", "#in($list $id 'id')?#end"); + String result = w.toString(); + assertEquals(result.split("\\?").length -1, 1); + assertEquals(result.split("IN").length -1, 1); + } + + @Test + public void ensureInClauseHasTwo() throws Exception { + StringWriter w = new StringWriter(); + ctxt.put("list", Arrays.asList("?", "?")); + velocity.evaluate(ctxt, w, "TEST", "#in($list $id 'id')?#end"); + String result = w.toString(); + assertEquals(result.split("\\?").length -1, 2); + } + + @Test + public void ensureInClauseHasOneThousand() throws Exception { + StringWriter w = new StringWriter(); + String[] arr = new String[1000]; + Arrays.fill(arr, "?"); + ctxt.put("list", Arrays.asList(arr)); + velocity.evaluate(ctxt, w, "TEST", "#in($list $id 'id')?#end"); + String result = w.toString(); + assertEquals(result.split("\\?").length -1, 1000); + assertEquals(result.split("OR").length -1, 0); + } + + @Test + public void ensureInClauseHasOneThousandAndOne() throws Exception { + StringWriter w = new StringWriter(); + String[] arr = new String[1001]; + Arrays.fill(arr, "?"); + ctxt.put("list", Arrays.asList(arr)); + velocity.evaluate(ctxt, w, "TEST", "#in($list $id 'id')?#end"); + String result = w.toString(); + assertEquals(result.split("\\?").length -1, 1001); + assertEquals(result.split("OR").length -1, 1); + } + + @Test + public void ensureInClauseHasTwoThousand() throws Exception { + StringWriter w = new StringWriter(); + String[] arr = new String[2000]; + Arrays.fill(arr, "?"); + ctxt.put("list", Arrays.asList(arr)); + velocity.evaluate(ctxt, w, "TEST", "#in($list $id 'id')?#end"); + String result = w.toString(); + assertEquals(result.split("\\?").length -1, 2000); + assertEquals(result.split("OR").length -1, 1); + } + + @Test + public void ensureInClauseHasTwoThousandAndOne() throws Exception { + StringWriter w = new StringWriter(); + String[] arr = new String[2001]; + Arrays.fill(arr, "?"); + ctxt.put("list", Arrays.asList(arr)); + velocity.evaluate(ctxt, w, "TEST", "#in($list $id 'id')?#end"); + String result = w.toString(); + assertEquals(result.split("\\?").length -1, 2001); + assertEquals(result.split("OR").length -1, 2); + } + + @Test + public void ensureInClauseHasThreeThousandAndOne() throws Exception { + StringWriter w = new StringWriter(); + String[] arr = new String[3001]; + Arrays.fill(arr, "?"); + ctxt.put("list", Arrays.asList(arr)); + velocity.evaluate(ctxt, w, "TEST", "#in($list $id 'id')?#end"); + String result = w.toString(); + assertEquals(result.split("\\?").length -1, 3001); + assertEquals(result.split("OR").length -1, 3); + } + +} diff --git a/src/test/java/org/mybatis/scripting/velocity/use/VelocityLanguageTest.java b/src/test/java/org/mybatis/scripting/velocity/use/VelocityLanguageTest.java index 92fa245..08aa7e7 100644 --- a/src/test/java/org/mybatis/scripting/velocity/use/VelocityLanguageTest.java +++ b/src/test/java/org/mybatis/scripting/velocity/use/VelocityLanguageTest.java @@ -67,7 +67,6 @@ public static void setUp() throws Exception { } @Test - @SuppressWarnings("unchecked") public void testDynamicSelectWithPropertyParams() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { @@ -99,7 +98,6 @@ public void testDynamicSelectWithPropertyParams() { } @Test - @SuppressWarnings("unchecked") public void testDynamicSelectWithExpressionParams() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { @@ -165,13 +163,12 @@ public void testSelectNamesWithFormattedParamSafe() { } @Test - @SuppressWarnings("unchecked") public void testDynamicSelectWithIteration() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { int[] ids = {2,4,5}; - Map param = new HashMap(); + Map param = new HashMap(); param.put("ids", ids); List answer = sqlSession.selectList("org.mybatis.scripting.velocity.use.selectNamesWithIteration", param); assertEquals(3, answer.size()); @@ -185,7 +182,6 @@ public void testDynamicSelectWithIteration() { } @Test - @SuppressWarnings("unchecked") public void testDynamicSelectWithIterationOverMap() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { @@ -194,7 +190,7 @@ public void testDynamicSelectWithIterationOverMap() { ids.put(2, "Wilma"); ids.put(4, "Barney"); ids.put(5, "Betty"); - Map param = new HashMap(); + Map> param = new HashMap>(); param.put("ids", ids); List answer = sqlSession.selectList("org.mybatis.scripting.velocity.use.selectNamesWithIterationOverMap", param); assertEquals(3, answer.size()); @@ -208,13 +204,12 @@ public void testDynamicSelectWithIterationOverMap() { } @Test - @SuppressWarnings("unchecked") public void testDynamicSelectWithIterationComplex() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { Name[] names = {new Name(2), new Name(4), new Name(5)}; - Map param = new HashMap(); + Map param = new HashMap(); param.put("names", names); List answer = sqlSession.selectList("org.mybatis.scripting.velocity.use.selectNamesWithIterationComplex", param); assertEquals(3, answer.size()); @@ -274,4 +269,88 @@ public void testSelectWithCustomUserDirective() { } } + @Test + public void testDynamicSelectWithInDirectiveForOneThousandPlusOne() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + try { + List names = new ArrayList(); + for (int i = 0; i < 1001; i++) { + names.add(new Name(i + 1)); + } + + Map> param = new HashMap>(); + param.put("names", names); + List answer = sqlSession + .selectList( + "org.mybatis.scripting.velocity.use.selectNamesWithInDirective", + param); + assertEquals(5, answer.size()); + } finally { + sqlSession.close(); + } + } + + @Test + public void testDynamicSelectWithInDirectiveForOneThousand() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + try { + List names = new ArrayList(); + for (int i = 0; i < 1000; i++) { + names.add(new Name(i + 1)); + } + + Map> param = new HashMap>(); + param.put("names", names); + List answer = sqlSession + .selectList( + "org.mybatis.scripting.velocity.use.selectNamesWithInDirective", + param); + assertEquals(5, answer.size()); + } finally { + sqlSession.close(); + } + } + + @Test + public void testDynamicSelectWithInDirectiveForOneThousandMinusOne() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + try { + List names = new ArrayList(); + for (int i = 0; i < 999; i++) { + names.add(new Name(i + 1)); + } + + Map> param = new HashMap>(); + param.put("names", names); + List answer = sqlSession + .selectList( + "org.mybatis.scripting.velocity.use.selectNamesWithInDirective", + param); + assertEquals(5, answer.size()); + } finally { + sqlSession.close(); + } + } + + @Test + public void testDynamicSelectWithInDirectiveForOneItem() { + SqlSession sqlSession = sqlSessionFactory.openSession(); + try { + List names = new ArrayList(); + for (int i = 0; i < 1; i++) { + names.add(new Name(i + 1)); + } + + Map> param = new HashMap>(); + param.put("names", names); + List answer = sqlSession + .selectList( + "org.mybatis.scripting.velocity.use.selectNamesWithInDirective", + param); + assertEquals(1, answer.size()); + } finally { + sqlSession.close(); + } + } + } diff --git a/src/test/java/org/mybatis/scripting/velocity/use/mapper.xml b/src/test/java/org/mybatis/scripting/velocity/use/mapper.xml index 3282b69..68c9580 100644 --- a/src/test/java/org/mybatis/scripting/velocity/use/mapper.xml +++ b/src/test/java/org/mybatis/scripting/velocity/use/mapper.xml @@ -90,6 +90,15 @@ #end + + CALL IDENTITY()