001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.jdbc;
017
018import java.sql.ResultSetMetaData;
019import java.sql.SQLException;
020import javax.jcr.PropertyType;
021import javax.jcr.RepositoryException;
022import javax.jcr.nodetype.NodeType;
023import javax.jcr.nodetype.PropertyDefinition;
024import javax.jcr.query.QueryResult;
025import org.modeshape.jdbc.JcrType.DefaultDataTypes;
026
027/**
028 * This driver's {@link ResultSetMetaData} implementation that obtains the metadata information from the JCR query result and
029 * (where possible) the column's corresponding property definitions.
030 */
031public class JcrResultSetMetaData implements ResultSetMetaData {
032
033    private final JcrConnection connection;
034    private final QueryResult results;
035    private int[] nullable;
036
037    protected JcrResultSetMetaData( JcrConnection connection,
038                                    QueryResult results ) {
039        this.connection = connection;
040        this.results = results;
041    }
042
043    /**
044     * {@inheritDoc}
045     * <p>
046     * All columns come from the same repository (i.e., catalog).
047     * </p>
048     * 
049     * @see java.sql.ResultSetMetaData#getCatalogName(int)
050     */
051    @Override
052    public String getCatalogName( int column ) {
053        return connection.info().getRepositoryName();
054    }
055
056    @Override
057    public String getColumnClassName( int column ) {
058        return getJcrType(column).getRepresentationClass().getName();
059    }
060
061    @Override
062    public int getColumnCount() throws SQLException {
063        try {
064            return results.getColumnNames().length;
065        } catch (RepositoryException e) {
066            throw new SQLException(e.getLocalizedMessage(), e);
067        }
068    }
069
070    /**
071     * {@inheritDoc}
072     * <p>
073     * This method returns the nominal display size based upon the column's type. Therefore, the value may not reflect the optimal
074     * display size for any given <i>value</i>.
075     * </p>
076     * 
077     * @see java.sql.ResultSetMetaData#getColumnDisplaySize(int)
078     */
079    @Override
080    public int getColumnDisplaySize( int column ) {
081        return getJcrType(column).getNominalDisplaySize();
082    }
083
084    @Override
085    public String getColumnLabel( int column ) throws SQLException {
086        try {
087            return results.getColumnNames()[column - 1]; // column value is 1-based
088        } catch (RepositoryException e) {
089            throw new SQLException(e.getLocalizedMessage(), e);
090        }
091    }
092
093    @Override
094    public String getColumnName( int column ) throws SQLException {
095        return getColumnLabel(column);
096    }
097
098    @Override
099    public int getColumnType( int column ) {
100        return getJcrType(column).getJdbcType();
101    }
102
103    @Override
104    public String getColumnTypeName( int column ) {
105        return getJcrType(column).getJdbcTypeName();
106    }
107
108    /**
109     * {@inheritDoc}
110     * <p>
111     * This method always returns the nominal display size for the type.
112     * </p>
113     * 
114     * @see java.sql.ResultSetMetaData#getPrecision(int)
115     */
116    @Override
117    public int getPrecision( int column ) {
118        return getJcrType(column).getNominalDisplaySize();
119    }
120
121    /**
122     * {@inheritDoc}
123     * <p>
124     * This method returns the number of digits behind the decimal point, which is assumed to be 3 if the type is
125     * {@link PropertyType#DOUBLE} or 0 otherwise.
126     * </p>
127     * 
128     * @see java.sql.ResultSetMetaData#getScale(int)
129     */
130    @Override
131    public int getScale( int column ) {
132        JcrType typeInfo = getJcrType(column);
133        if (typeInfo.getJcrType() == PropertyType.DOUBLE) {
134            return 3; // pulled from thin air
135        }
136        return 0;
137    }
138
139    /**
140     * {@inheritDoc}
141     * <p>
142     * This method always returns the workspace name.
143     * </p>
144     * 
145     * @see java.sql.ResultSetMetaData#getSchemaName(int)
146     */
147    @Override
148    public String getSchemaName( int column ) {
149        return connection.info().getWorkspaceName();
150    }
151
152    @Override
153    public String getTableName( int column ) throws SQLException {
154        try {
155            return results.getSelectorNames()[column - 1]; // column value is 1-based
156        } catch (RepositoryException e) {
157            throw new SQLException(e.getLocalizedMessage(), e);
158        }
159    }
160
161    /**
162     * {@inheritDoc}
163     * <p>
164     * This method always returns false, since this JCR property types don't represent auto-incremented values.
165     * </p>
166     * 
167     * @see java.sql.ResultSetMetaData#isAutoIncrement(int)
168     */
169    @Override
170    public boolean isAutoIncrement( int column ) {
171        return false;
172    }
173
174    @Override
175    public boolean isCaseSensitive( int column ) {
176        return getJcrType(column).isCaseSensitive();
177    }
178
179    /**
180     * {@inheritDoc}
181     * <p>
182     * This method always returns false, since no JCR property types (directly) represent currency.
183     * </p>
184     * 
185     * @see java.sql.ResultSetMetaData#isCurrency(int)
186     */
187    @Override
188    public boolean isCurrency( int column ) {
189        return false;
190    }
191
192    /**
193     * {@inheritDoc}
194     * <p>
195     * This method always returns false, since this JDBC driver does not support writes.
196     * </p>
197     * 
198     * @see java.sql.ResultSetMetaData#isDefinitelyWritable(int)
199     */
200    @Override
201    public boolean isDefinitelyWritable( int column ) {
202        return false;
203    }
204
205    @Override
206    public int isNullable( int column ) throws SQLException {
207        if (nullable == null) {
208            int length = getColumnCount();
209            nullable = new int[length];
210            for (int i = 0; i != length; ++i) {
211                nullable[i] = -1;
212            }
213        } else {
214            int result = nullable[column - 1];
215            if (result != -1) {
216                // Already found this value, so return it ...
217                return result;
218            }
219        }
220        // Find the node type for the column (given that the column name is the property name and
221        // the table name is the node type), and determine if the property definition is multi-valued or not mandatory.
222        String nodeTypeName = getTableName(column);
223        if (nodeTypeName.length() == 0) {
224            // There is no table for the column, so therefore we don't know the node type ...
225            return ResultSetMetaData.columnNullableUnknown;
226        }
227        String propertyName = getColumnName(column);
228        boolean singleProp = false;
229        boolean singleResidual = false;
230        boolean multiResidual = false;
231        NodeType type = connection.nodeType(nodeTypeName);
232        for (PropertyDefinition defn : type.getPropertyDefinitions()) {
233            if (defn.getName().equals(propertyName)) {
234                if (defn.isMultiple() || defn.isMandatory()) {
235                    // We know this IS nullable
236                    return ResultSetMetaData.columnNullable;
237                }
238                // Otherwise this is a single-valued property that is mandatory,
239                // but we can't return columnNotNullable because we may not have found the multi-valued property ...
240                singleProp = true;
241            } else if (defn.getName().equals("*")) { // Residual
242                if (defn.isMultiple() || defn.isMandatory()) multiResidual = true;
243                else singleResidual = true;
244            }
245        }
246        int result = ResultSetMetaData.columnNullableUnknown;
247        if (multiResidual) result = ResultSetMetaData.columnNullable;
248        else if (singleProp || singleResidual) result = ResultSetMetaData.columnNoNulls;
249        nullable[column - 1] = result;
250        return result;
251    }
252
253    /**
254     * {@inheritDoc}
255     * <p>
256     * Even though the value may be writable in the JCR repository, this JDBC driver does not support writes. Therefore, this
257     * method always returns true.
258     * </p>
259     * 
260     * @see java.sql.ResultSetMetaData#isReadOnly(int)
261     */
262    @Override
263    public boolean isReadOnly( int column ) {
264        return true;
265    }
266
267    /**
268     * {@inheritDoc}
269     * <p>
270     * In JCR-SQL2, every property can be used in a WHERE clause. Therefore, this method always returns true.
271     * </p>
272     * 
273     * @see java.sql.ResultSetMetaData#isSearchable(int)
274     */
275    @Override
276    public boolean isSearchable( int column ) {
277        return true;
278    }
279
280    /**
281     * {@inheritDoc}
282     * <p>
283     * This method returns true if the column is a {@link PropertyType#DOUBLE}, {@link PropertyType#LONG} or
284     * {@link PropertyType#DATE}.
285     * </p>
286     * 
287     * @see java.sql.ResultSetMetaData#isSigned(int)
288     */
289    @Override
290    public boolean isSigned( int column ) {
291        return getJcrType(column).isSigned();
292    }
293
294    /**
295     * {@inheritDoc}
296     * <p>
297     * Even though the value may be writable in the JCR repository, this JDBC driver does not support writes. Therefore, this
298     * method always returns false.
299     * </p>
300     * 
301     * @see java.sql.ResultSetMetaData#isWritable(int)
302     */
303    @Override
304    public boolean isWritable( int column ) {
305        return false;
306    }
307
308    @Override
309    public boolean isWrapperFor( Class<?> iface ) /*throws SQLException*/{
310        return iface.isInstance(results);
311    }
312
313    @Override
314    public <T> T unwrap( Class<T> iface ) throws SQLException {
315        if (iface.isInstance(results)) {
316            return iface.cast(results);
317        }
318        throw new SQLException(JdbcLocalI18n.classDoesNotImplementInterface.text(ResultSetMetaData.class.getSimpleName(),
319                                                                                 iface.getName()));
320    }
321
322    private JcrType getJcrType( int column ) {
323        JcrType typeInfo = null;
324        if (results instanceof org.modeshape.jcr.api.query.QueryResult) {
325            final String typeName = ((org.modeshape.jcr.api.query.QueryResult)results).getColumnTypes()[column - 1];
326            typeInfo = JcrType.typeInfo(typeName);
327        }
328        /**
329         * If no type is matched, then default to STRING
330         */
331        return (typeInfo != null ? typeInfo : JcrType.typeInfo(DefaultDataTypes.STRING));
332    }
333
334}