/* * Copyright 2004 Clinton Begin * * 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 com.ibatis.sqlmap.engine.mapping.result; import com.ibatis.common.beans.Probe; import com.ibatis.common.beans.ProbeFactory; import com.ibatis.common.jdbc.exception.NestedSQLException; import com.ibatis.sqlmap.client.SqlMapException; import com.ibatis.sqlmap.engine.exchange.DataExchange; import com.ibatis.sqlmap.engine.impl.SqlMapClientImpl; import com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate; import com.ibatis.sqlmap.engine.mapping.result.loader.ResultLoader; import com.ibatis.sqlmap.engine.mapping.sql.Sql; import com.ibatis.sqlmap.engine.mapping.statement.MappedStatement; import com.ibatis.sqlmap.engine.scope.ErrorContext; import com.ibatis.sqlmap.engine.scope.StatementScope; import com.ibatis.sqlmap.engine.type.DomCollectionTypeMarker; import com.ibatis.sqlmap.engine.type.DomTypeMarker; import com.ibatis.sqlmap.engine.type.TypeHandler; import com.ibatis.sqlmap.engine.type.TypeHandlerFactory; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; /** * Basic implementation of ResultMap interface */ public class ResultMap { private static final Probe PROBE = ProbeFactory.getProbe(); private static final String KEY_SEPARATOR = "\002"; private String id; private Class resultClass; // DO NOT ACCESS EITHER OF THESE OUTSIDE OF THEIR BEAN GETTER/SETTER private ResultMapping[] resultMappings; private ThreadLocal remappableResultMappings = new ThreadLocal(); private DataExchange dataExchange; private List nestedResultMappings; private Discriminator discriminator; private Set groupByProps; private String xmlName; private String resource; protected SqlMapExecutorDelegate delegate; protected boolean allowRemapping = false; public static final Object NO_VALUE = new Object(); /** * Constructor to pass a SqlMapExecutorDelegate in * * @param delegate - the SqlMapExecutorDelegate */ public ResultMap(SqlMapExecutorDelegate delegate) { this.delegate = delegate; } /** * Getter for the SqlMapExecutorDelegate * * @return - the delegate */ public SqlMapExecutorDelegate getDelegate() { return delegate; } public String getId() { return id; } /** * Setter for the ID * * @param id - the new ID */ public void setId(String id) { this.id = id; } public Class getResultClass() { return resultClass; } public Object getUniqueKey(String keyPrefix, Object[] values) { if (groupByProps != null) { StringBuffer keyBuffer; if ( keyPrefix != null ) keyBuffer = new StringBuffer(keyPrefix); else keyBuffer = new StringBuffer(); for (int i = 0; i < getResultMappings().length; i++) { String propertyName = getResultMappings()[i].getPropertyName(); if (groupByProps.contains(propertyName)) { keyBuffer.append(values[i]); keyBuffer.append('-'); } } if (keyBuffer.length() < 1) { return null; } else { // seperator value not likely to appear in a database keyBuffer.append(KEY_SEPARATOR); return keyBuffer.toString(); } } else { return null; } } public Object getUniqueKey(Object[] values) { return getUniqueKey(null, values); } /** * Setter for the result class (what the results will be mapped into) * * @param resultClass - the result class */ public void setResultClass(Class resultClass) { this.resultClass = resultClass; } /** * Getter for the DataExchange object to be used * * @return - the DataExchange object */ public DataExchange getDataExchange() { return dataExchange; } /** * Setter for the DataExchange object to be used * * @param dataExchange - the new DataExchange object */ public void setDataExchange(DataExchange dataExchange) { this.dataExchange = dataExchange; } /** * Getter (used by DomDataExchange) for the xml name of the results * * @return - the name */ public String getXmlName() { return xmlName; } /** * Setter (used by the SqlMapBuilder) for the xml name of the results * * @param xmlName - the name */ public void setXmlName(String xmlName) { this.xmlName = xmlName; } /** * Getter for the resource (used to report errors) * * @return - the resource */ public String getResource() { return resource; } /** * Setter for the resource (used by the SqlMapBuilder) * * @param resource - the resource name */ public void setResource(String resource) { this.resource = resource; } public void addGroupByProperty(String name) { if (groupByProps == null) { groupByProps = new HashSet(); } groupByProps.add(name); } public boolean hasGroupBy() { return groupByProps != null && groupByProps.size() > 0; } public Iterator groupByProps() { return groupByProps.iterator(); } public void addNestedResultMappings(ResultMapping mapping) { if (nestedResultMappings == null) { nestedResultMappings = new ArrayList(); } nestedResultMappings.add(mapping); } public List getNestedResultMappings() { return nestedResultMappings; } public ResultMapping[] getResultMappings() { if (allowRemapping) { return (ResultMapping[]) remappableResultMappings.get(); } else { return resultMappings; } } public void setDiscriminator (Discriminator discriminator) { if (this.discriminator != null) { throw new SqlMapException ("A discriminator may only be set once per result map."); } this.discriminator = discriminator; } public Discriminator getDiscriminator() { return discriminator; } public ResultMap resolveSubMap (StatementScope statementScope, ResultSet rs) throws SQLException { ResultMap subMap = this; if (discriminator != null) { ResultMapping mapping = (ResultMapping)discriminator.getResultMapping(); Object value = getPrimitiveResultMappingValue(rs, mapping); if (value == null) { value = doNullMapping(value, mapping); } subMap = discriminator.getSubMap(String.valueOf(value)); if (subMap == null) { subMap = this; } else if (subMap != this) { subMap = subMap.resolveSubMap(statementScope, rs); } } return subMap; } /** * Setter for a list of the individual ResultMapping objects * * @param resultMappingList - the list */ public void setResultMappingList(List resultMappingList) { if (allowRemapping) { this.remappableResultMappings.set((ResultMapping[]) resultMappingList.toArray(new ResultMapping[resultMappingList.size()])); } else { this.resultMappings = (ResultMapping[]) resultMappingList.toArray(new ResultMapping[resultMappingList.size()]); } Map props = new HashMap(); props.put("map", this); dataExchange = getDelegate().getDataExchangeFactory().getDataExchangeForClass(resultClass); dataExchange.initialize(props); } /** * Getter for the number of ResultMapping objects * * @return - the count */ public int getResultCount() { return this.getResultMappings().length; } /** * Read a row from a resultset and map results to an array. * * @param statementScope scope of the request * @param rs ResultSet to read from * * @return row read as an array of column values. * * @throws java.sql.SQLException */ public Object[] getResults(StatementScope statementScope, ResultSet rs) throws SQLException { ErrorContext errorContext = statementScope.getErrorContext(); errorContext.setActivity("applying a result map"); errorContext.setObjectId(this.getId()); errorContext.setResource(this.getResource()); errorContext.setMoreInfo("Check the result map."); boolean foundData = false; Object[] columnValues = new Object[getResultMappings().length]; for (int i = 0; i < getResultMappings().length; i++) { ResultMapping mapping = (ResultMapping) getResultMappings()[i]; errorContext.setMoreInfo(mapping.getErrorString()); if (mapping.getStatementName() != null) { if (resultClass == null) { throw new SqlMapException("The result class was null when trying to get results for ResultMap named " + getId() + "."); } else if (Map.class.isAssignableFrom(resultClass)) { Class javaType = mapping.getJavaType(); if (javaType == null) { javaType = Object.class; } columnValues[i] = getNestedSelectMappingValue(statementScope, rs, mapping, javaType); } else if (DomTypeMarker.class.isAssignableFrom(resultClass)) { Class javaType = mapping.getJavaType(); if (javaType == null) { javaType = DomTypeMarker.class; } columnValues[i] = getNestedSelectMappingValue(statementScope, rs, mapping, javaType); } else { Probe p = ProbeFactory.getProbe(resultClass); Class type = p.getPropertyTypeForSetter(resultClass, mapping.getPropertyName()); columnValues[i] = getNestedSelectMappingValue(statementScope, rs, mapping, type); } foundData = foundData || columnValues[i] != null; } else if (mapping.getNestedResultMapName() == null) { columnValues[i] = getPrimitiveResultMappingValue(rs, mapping); if (columnValues[i] == null) { columnValues[i] = doNullMapping(columnValues[i], mapping); } else { foundData = true; } } } statementScope.setRowDataFound(foundData); return columnValues; } public Object setResultObjectValues(StatementScope statementScope, Object resultObject, Object[] values) { final String previousNestedKey = statementScope.getCurrentNestedKey(); String ukey = (String)getUniqueKey(statementScope.getCurrentNestedKey(), values); Map uniqueKeys = statementScope.getUniqueKeys(this); statementScope.setCurrentNestedKey(ukey); if (uniqueKeys != null && uniqueKeys.containsKey(ukey)) { // Unique key is already known, so get the existing result object and process additional results. resultObject = uniqueKeys.get(ukey); applyNestedResultMap(statementScope, resultObject, values); resultObject = NO_VALUE; } else if (ukey == null || uniqueKeys == null || !uniqueKeys.containsKey(ukey)) { // Unique key is NOT known, so create a new result object and then process additional results. resultObject = dataExchange.setData(statementScope, this, resultObject, values); // Lazy init key set, only if we're grouped by something (i.e. ukey != null) if (ukey != null) { if (uniqueKeys == null) { uniqueKeys = new HashMap(); statementScope.setUniqueKeys(this, uniqueKeys); } uniqueKeys.put(ukey, resultObject); } applyNestedResultMap(statementScope, resultObject, values); } else { // Otherwise, we don't care about these results. resultObject = NO_VALUE; } statementScope.setCurrentNestedKey(previousNestedKey); return resultObject; } private void applyNestedResultMap(StatementScope statementScope, Object resultObject, Object[] values) { if (resultObject != null && resultObject != NO_VALUE) { if (nestedResultMappings != null) { for (int i = 0, n = nestedResultMappings.size(); i < n; i++) { ResultMapping resultMapping = (ResultMapping) nestedResultMappings.get(i); setNestedResultMappingValue(resultMapping, statementScope, resultObject, values); } } } } /** * Some changes in this method for IBATIS-225: * <ul> * <li>We no longer require the nested property to be a collection. This * will allow reuses of resultMaps on 1:1 relationships</li> * <li>If the nested property is not a collection, then it will be * created/replaced by the values generated from the current row.</li> * </ul> * * @param mapping * @param statementScope * @param resultObject * @param values */ protected void setNestedResultMappingValue(ResultMapping mapping, StatementScope statementScope, Object resultObject, Object[] values) { try { String resultMapName = mapping.getNestedResultMapName(); ResultMap resultMap = getDelegate().getResultMap(resultMapName); // get the discriminated submap if it exists resultMap = resultMap.resolveSubMap(statementScope, statementScope.getResultSet()); Class type = mapping.getJavaType(); String propertyName = mapping.getPropertyName(); Object obj = PROBE.getObject(resultObject, propertyName); if (obj == null) { if (type == null) { type = PROBE.getPropertyTypeForSetter(resultObject, propertyName); } try { // create the object if is it a Collection. If not a Collection // then we will just set the property to the object created // in processing the nested result map if (Collection.class.isAssignableFrom(type)) { obj = ResultObjectFactoryUtil.createObjectThroughFactory(type); PROBE.setObject(resultObject, propertyName, obj); } } catch (Exception e) { throw new SqlMapException("Error instantiating collection property for mapping '" + mapping.getPropertyName() + "'. Cause: " + e, e); } } //JIRA 375 "Provide a way for not creating items from nested ResultMaps when the items contain only null values" boolean subResultObjectAbsent = false; if(mapping.getNotNullColumn() != null) { if(statementScope.getResultSet().getObject(mapping.getNotNullColumn()) == null) { subResultObjectAbsent = true; } } if(!subResultObjectAbsent) { values = resultMap.getResults(statementScope, statementScope.getResultSet()); if (statementScope.isRowDataFound()) { Object o = resultMap.setResultObjectValues(statementScope, null, values); if (o != NO_VALUE) { if (obj != null && obj instanceof Collection) { ((Collection) obj).add(o); } else { PROBE.setObject(resultObject, propertyName, o); } } } } } catch (SQLException e) { throw new SqlMapException("Error getting nested result map values for '" + mapping.getPropertyName() + "'. Cause: " + e, e); } } protected Object getNestedSelectMappingValue(StatementScope statementScope, ResultSet rs, ResultMapping mapping, Class targetType) throws SQLException { try { TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory(); String statementName = mapping.getStatementName(); SqlMapClientImpl client = (SqlMapClientImpl) statementScope.getSession().getSqlMapClient(); MappedStatement mappedStatement = client.getMappedStatement(statementName); Class parameterType = mappedStatement.getParameterClass(); Object parameterObject = null; if (parameterType == null) { parameterObject = prepareBeanParameterObject(statementScope, rs, mapping, parameterType); } else { if (typeHandlerFactory.hasTypeHandler(parameterType)) { parameterObject = preparePrimitiveParameterObject(rs, mapping, parameterType); } else if (DomTypeMarker.class.isAssignableFrom(parameterType)) { parameterObject = prepareDomParameterObject(rs, mapping); } else { parameterObject = prepareBeanParameterObject(statementScope, rs, mapping, parameterType); } } Object result = null; if (parameterObject != null) { Sql sql = mappedStatement.getSql(); ResultMap resultMap = sql.getResultMap(statementScope, parameterObject); Class resultClass = resultMap.getResultClass(); if (resultClass != null && !DomTypeMarker.class.isAssignableFrom(targetType)) { if (DomCollectionTypeMarker.class.isAssignableFrom(resultClass)) { targetType = DomCollectionTypeMarker.class; } else if (DomTypeMarker.class.isAssignableFrom(resultClass)) { targetType = DomTypeMarker.class; } } result = ResultLoader.loadResult(client, statementName, parameterObject, targetType); String nullValue = mapping.getNullValue(); if (result == null && nullValue != null) { TypeHandler typeHandler = typeHandlerFactory.getTypeHandler(targetType); if (typeHandler != null) { result = typeHandler.valueOf(nullValue); } } } return result; } catch (InstantiationException e) { throw new NestedSQLException("Error setting nested bean property. Cause: " + e, e); } catch (IllegalAccessException e) { throw new NestedSQLException("Error setting nested bean property. Cause: " + e, e); } } private Object preparePrimitiveParameterObject(ResultSet rs, ResultMapping mapping, Class parameterType) throws SQLException { Object parameterObject; TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory(); TypeHandler th = typeHandlerFactory.getTypeHandler(parameterType); parameterObject = th.getResult(rs, mapping.getColumnName()); return parameterObject; } private Document newDocument(String root) { try { Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); doc.appendChild(doc.createElement(root)); return doc; } catch (ParserConfigurationException e) { throw new RuntimeException("Error creating XML document. Cause: " + e); } } private Object prepareDomParameterObject(ResultSet rs, ResultMapping mapping) throws SQLException { TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory(); Document doc = newDocument("parameter"); Probe probe = ProbeFactory.getProbe(doc); String complexName = mapping.getColumnName(); TypeHandler stringTypeHandler = typeHandlerFactory.getTypeHandler(String.class); if (complexName.indexOf('=') > -1) { // old 1.x style multiple params StringTokenizer parser = new StringTokenizer(complexName, "{}=, ", false); while (parser.hasMoreTokens()) { String propName = parser.nextToken(); String colName = parser.nextToken(); Object propValue = stringTypeHandler.getResult(rs, colName); probe.setObject(doc, propName, propValue.toString()); } } else { // single param Object propValue = stringTypeHandler.getResult(rs, complexName); probe.setObject(doc, "value", propValue.toString()); } return doc; } private Object prepareBeanParameterObject(StatementScope statementScope, ResultSet rs, ResultMapping mapping, Class parameterType) throws InstantiationException, IllegalAccessException, SQLException { TypeHandlerFactory typeHandlerFactory = getDelegate().getTypeHandlerFactory(); Object parameterObject; if (parameterType == null) { parameterObject = new HashMap(); } else { parameterObject = ResultObjectFactoryUtil.createObjectThroughFactory(parameterType); } String complexName = mapping.getColumnName(); if (complexName.indexOf('=') > -1 || complexName.indexOf(',') > -1) { StringTokenizer parser = new StringTokenizer(complexName, "{}=, ", false); while (parser.hasMoreTokens()) { String propName = parser.nextToken(); String colName = parser.nextToken(); Class propType = PROBE.getPropertyTypeForSetter(parameterObject, propName); TypeHandler propTypeHandler = typeHandlerFactory.getTypeHandler(propType); Object propValue = propTypeHandler.getResult(rs, colName); PROBE.setObject(parameterObject, propName, propValue); } } else { // single param TypeHandler propTypeHandler = typeHandlerFactory.getTypeHandler(parameterType); if (propTypeHandler == null) { propTypeHandler = typeHandlerFactory.getUnkownTypeHandler(); } parameterObject = propTypeHandler.getResult(rs, complexName); } return parameterObject; } protected Object getPrimitiveResultMappingValue(ResultSet rs, ResultMapping mapping) throws SQLException { Object value = null; TypeHandler typeHandler = mapping.getTypeHandler(); if (typeHandler != null) { String columnName = mapping.getColumnName(); int columnIndex = mapping.getColumnIndex(); if (columnName == null) { value = typeHandler.getResult(rs, columnIndex); } else { value = typeHandler.getResult(rs, columnName); } } else { throw new SqlMapException("No type handler could be found to map the property '" + mapping.getPropertyName() + "' to the column '" + mapping.getColumnName() + "'. One or both of the types, or the combination of types is not supported."); } return value; } protected Object doNullMapping(Object value, ResultMapping mapping) throws SqlMapException { if ( value == null ) { TypeHandler typeHandler = mapping.getTypeHandler(); if (typeHandler != null) { String nullValue = mapping.getNullValue(); if ( nullValue != null ) value = typeHandler.valueOf(nullValue); return value; } else { throw new SqlMapException("No type handler could be found to map the property '" + mapping.getPropertyName() + "' to the column '" + mapping.getColumnName() + "'. One or both of the types, or the combination of types is not supported."); } } else { return value; } } }