/***************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.cayenne.access.types; import org.apache.cayenne.CayenneException; import java.io.IOException; import java.io.Reader; import java.io.StringWriter; import java.sql.CallableStatement; import java.sql.Clob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; /** * Handles <code>java.lang.String</code>, mapping it as either of JDBC types - * CLOB or (VAR)CHAR. Can be configured to trim trailing spaces. */ public class CharType implements ExtendedType<String> { private static final int BUF_SIZE = 8 * 1024; private static final int TRIM_VALUES_THRESHOLD = 30; protected boolean trimmingChars; protected boolean usingClobs; public CharType(boolean trimmingChars, boolean usingClobs) { this.trimmingChars = trimmingChars; this.usingClobs = usingClobs; } /** * Returns "java.lang.String". */ @Override public String getClassName() { return String.class.getName(); } /** Return trimmed string. */ @Override public String materializeObject(ResultSet rs, int index, int type) throws Exception { if (type == Types.CLOB || type == Types.NCLOB) { return isUsingClobs() ? readClob(rs.getClob(index)) : readCharStream(rs, index); } return handleString(rs.getString(index), type); } @Override public String materializeObject(CallableStatement cs, int index, int type) throws Exception { if (type == Types.CLOB || type == Types.NCLOB) { if (!isUsingClobs()) { throw new CayenneException("Character streams are not supported in stored procedure parameters."); } return readClob(cs.getClob(index)); } return handleString(cs.getString(index), type); } private String handleString(String val, int type) throws SQLException { // trim CHAR type if (val != null && (type == Types.CHAR || type == Types.NCHAR) && isTrimmingChars()) { return rtrim(val); } return val; } /** Trim right spaces. */ protected String rtrim(String value) { int end = value.length() - 1; int count = end; while (end >= 0 && value.charAt(end) <= ' ') { end--; } return end == count ? value : value.substring(0, end + 1); } @Override public void setJdbcObject(PreparedStatement st, String value, int pos, int type, int scale) throws Exception { // if this is a CLOB column, set the value as "String" // instead. This should work with most drivers if (type == Types.CLOB || type == Types.NCLOB) { st.setString(pos, value); } else if (scale != -1) { st.setObject(pos, value, type, scale); } else { st.setObject(pos, value, type); } } @Override public String toString(String value) { if (value == null) { return "NULL"; } StringBuilder buffer = new StringBuilder(); buffer.append('\''); String literal = value; if (literal.length() > TRIM_VALUES_THRESHOLD) { literal = literal.substring(0, TRIM_VALUES_THRESHOLD) + "..."; } // escape quotes int curPos = 0; int endPos = 0; while ((endPos = literal.indexOf('\'', curPos)) >= 0) { buffer.append(literal.substring(curPos, endPos + 1)).append('\''); curPos = endPos + 1; } if (curPos < literal.length()) { buffer.append(literal.substring(curPos)); } buffer.append('\''); return buffer.toString(); } protected String readClob(Clob clob) throws IOException, SQLException { if (clob == null) { return null; } // sanity check on size if (clob.length() > Integer.MAX_VALUE) { throw new IllegalArgumentException("CLOB is too big to be read as String in memory: " + clob.length()); } int size = (int) clob.length(); if (size == 0) { return ""; } return clob.getSubString(1, size); } protected String readCharStream(ResultSet rs, int index) throws IOException, SQLException { try (Reader in = rs.getCharacterStream(index);) { return in != null ? readValueStream(in, -1, BUF_SIZE) : null; } } protected String readValueStream(Reader in, int streamSize, int bufSize) throws IOException { char[] buf = new char[bufSize]; StringWriter out = streamSize > 0 ? new StringWriter(streamSize) : new StringWriter(); int read; while ((read = in.read(buf, 0, bufSize)) >= 0) { out.write(buf, 0, read); } return out.toString(); } /** * Returns <code>true</code> if 'materializeObject' method should trim * trailing spaces from the CHAR columns. This addresses an issue with some * JDBC drivers (e.g. Oracle), that return Strings for CHAR columns padded * with spaces. */ public boolean isTrimmingChars() { return trimmingChars; } public void setTrimmingChars(boolean trimingChars) { this.trimmingChars = trimingChars; } public boolean isUsingClobs() { return usingClobs; } public void setUsingClobs(boolean usingClobs) { this.usingClobs = usingClobs; } }