001/*
002 * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the The BSD 3-Clause License
006 * which accompanies this distribution, and is available at
007 * http://opensource.org/licenses/BSD-3-Clause
008 *
009 * Contributors:
010 *   Tada AB
011 *   Filip Hrbek
012 *   Chapman Flack
013 */
014package org.postgresql.pljava.example;
015
016import java.lang.reflect.InvocationTargetException;
017import java.lang.reflect.Method;
018import java.sql.Connection;
019import java.sql.DatabaseMetaData;
020import java.sql.DriverManager;
021import java.sql.ResultSet;
022import java.sql.ResultSetMetaData;
023import java.sql.SQLException;
024import java.util.ArrayList;
025import java.util.Iterator;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029/**
030 * Provides a {@link #callMetaDataMethod callMetaData} function taking a string
031 * that supplies the name of, and arguments to, any {@code ResultSet}-returning
032 * {@link DatabaseMetaData} method, and returns a single-column {@code varchar}
033 * result, the first row a header, then one for each {@code ResultSet} row,
034 * semicolons delimiting the original columns.
035 * @author Filip Hrbek
036 */
037public class MetaDataTest {
038    public static Iterator<String> callMetaDataMethod(String methodCall)
039            throws SQLException {
040        return new MetaDataTest(methodCall).iterator();
041    }
042
043    private String m_methodName;
044    private Object[] m_methodArgs;
045    private Class<?>[] m_methodArgTypes;
046    private ArrayList<String> m_results;
047
048    public MetaDataTest(String methodCall) throws SQLException {
049        Connection conn = DriverManager
050                .getConnection("jdbc:default:connection");
051        DatabaseMetaData md = conn.getMetaData();
052        ResultSet rs;
053        m_results = new ArrayList<>();
054        StringBuffer result;
055
056        parseMethodCall(methodCall);
057
058        try {
059            Method m = DatabaseMetaData.class.getMethod(m_methodName,
060                    m_methodArgTypes);
061            if (!m.getReturnType().equals(ResultSet.class)) {
062                throw new NoSuchMethodException("Unexpected return type");
063            }
064
065            rs = (ResultSet) m.invoke(md, m_methodArgs);
066            ResultSetMetaData rsmd = rs.getMetaData();
067
068            int cnt = rsmd.getColumnCount();
069            result = new StringBuffer();
070            for (int i = 1; i <= cnt; i++) {
071                result.append((rsmd.getColumnName(i) + "("
072                        + rsmd.getColumnClassName(i) + ")").replaceAll(
073                        "(\\\\|;)", "\\$1") + ";");
074            }
075            m_results.add(result.toString());
076
077            while (rs.next()) {
078                result = new StringBuffer();
079                Object rsObject = null;
080                for (int i = 1; i <= cnt; i++) {
081                    rsObject = rs.getObject(i);
082                    if (rsObject == null) {
083                        rsObject = "<NULL>";
084                    }
085
086                    result.append(rsObject.toString().replaceAll("(\\\\|;)",
087                            "\\$1")
088                            + ";");
089                }
090                m_results.add(result.toString());
091            }
092            rs.close();
093        } catch (NoSuchMethodException nme) {
094            StringBuffer sb = new StringBuffer();
095            for (int i = 0; i < m_methodArgTypes.length; i++) {
096                if (sb.length() > 0) {
097                    sb.append(",");
098                }
099                sb.append(m_methodArgTypes[i].getName());
100            }
101            throw new SQLException(
102                    "No such method or non-resultset return type: "
103                            + m_methodName + "(" + sb.toString() + ")");
104        } catch (InvocationTargetException ite) {
105            throw new SQLException(ite.getTargetException().toString());
106        } catch (Exception e) {
107            throw new SQLException("Method error: " + e.toString());
108        }
109    }
110
111    public void close() {
112    }
113
114    private Iterator<String> iterator() {
115        return m_results.iterator();
116    }
117
118    /**
119     * Method call parser. Let's say that only String, String[], int, int[] and
120     * boolean types are accepted. Examples: "foo" => String {"foo","bar"} =>
121     * String[] {} => String[] (zero length array, not null!) 10 => int -7 =>
122     * int [10,3] => int[] [] => int[] (zero length array, not null!) TRUE =>
123     * boolean (String)null => String (value null) (String[])null => String[]
124     * (value null) (int[])null => int[] (value null)
125     * 
126     * Complete example: select * from
127     * callMetaDataMethod('getTables((String)null,"sqlj","jar%",{"TABLE"})');
128     * 
129     * It makes the implementation simplier, and it is sufficient for
130     * DatabaseMetaData testing.
131     */
132    private void parseMethodCall(String methodCall) throws SQLException {
133        try {
134            Pattern p = Pattern
135                    .compile("^\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\((.*)\\)\\s*$");
136            Matcher m = p.matcher(methodCall);
137            String paramString;
138            String auxParamString;
139            String param;
140            ArrayList<Object> objects = new ArrayList<>();
141            ArrayList<Class<?>> types = new ArrayList<>();
142
143            if (m.matches()) {
144                m_methodName = m.group(1);
145                paramString = m.group(2).trim();
146                p = Pattern
147                        .compile("^\\s*("
148                                + "\\(\\s*(?:String|int)\\s*(?:\\[\\s*\\])?\\s*\\)\\s*null|"
149                                + // String, String[] or int[] null
150                                "TRUE|"
151                                + // boolean TRUE
152                                "FALSE|"
153                                + // boolean FALSE
154                                "(?:\\-|\\+)?[0-9]+|"
155                                + // int
156                                "\\[((?:[^\\[\\]])*)\\]|"
157                                + // int[]
158                                "\"((?:[^\\\\\"]|\\\\.)*)\"|"
159                                + // String
160                                "\\{((?:[^\\{\\}]|\"(?:[^\\\\\"]|\\\\.)*\")*)\\}"
161                                + // String[]
162                                ")\\s*" + "(?:,|$)" + // comma separator
163                                "(.*)$"); // rest of the string
164                auxParamString = paramString;
165                while (!auxParamString.equals("")) {
166                    m = p.matcher(auxParamString);
167                    if (!m.matches()) {
168                        throw new SQLException("Invalid parameter list: "
169                                + paramString);
170                    }
171
172                    param = m.group(1);
173                    if (param.startsWith("\"")) // it is a string
174                    {
175                        param = m.group(3); // string without the quotes
176                        objects.add(param);
177                        types.add(String.class);
178                    } else if (param.startsWith("{")) // it is a string array
179                    {
180                        param = m.group(4); // string array without the curly
181                                            // brackets
182                        Pattern parr = Pattern
183                                .compile("^\\s*\"((?:[^\\\\\"]|\\\\.)*)\"\\s*(?:,|$)(.*)$");
184                        Matcher marr;
185                        String auxParamArr = param.trim();
186                        ArrayList<String> strList = new ArrayList<>();
187
188                        while (!auxParamArr.equals("")) {
189                            marr = parr.matcher(auxParamArr);
190                            if (!marr.matches()) {
191                                throw new SQLException("Invalid string array: "
192                                        + param);
193                            }
194
195                            strList.add(marr.group(1));
196                            auxParamArr = marr.group(2).trim();
197                        }
198                        objects.add(strList.toArray(new String[0]));
199                        types.add(String[].class);
200                    } else if (param.equals("TRUE") || param.equals("FALSE")) // it
201                                                                                // is
202                                                                                // a
203                                                                                // boolean
204                    {
205                        objects.add(Boolean.valueOf(param));
206                        types.add(Boolean.TYPE);
207                    } else if (param.startsWith("(")) // it is String, String[]
208                                                        // or int[] null
209                    {
210                        Pattern pnull = Pattern
211                                .compile("^\\(\\s*(String|int)\\s*(\\[\\s*\\])?\\s*\\)\\s*null\\s*$");
212                        Matcher mnull = pnull.matcher(param);
213
214                        if (mnull.matches()) {
215                            objects.add(null);
216                            if (mnull.group(2) == null) {
217                                if (mnull.group(1).equals("String")) {
218                                    types.add(String.class);
219                                } else {
220                                    throw new SQLException(
221                                            "Primitive 'int' cannot be null");
222                                }
223                            } else {
224                                if (mnull.group(1).equals("String")) {
225                                    types.add(String[].class);
226                                } else {
227                                    types.add(int[].class);
228                                }
229                            }
230                        } else {
231                            throw new SQLException("Invalid null value: "
232                                    + param);
233                        }
234                    } else if (param.startsWith("[")) // it is a int array
235                    {
236                        param = m.group(2); // int array without the square
237                                            // brackets
238                        Pattern parr = Pattern
239                                .compile("^\\s*(\\d+)\\s*(?:,|$)(.*)$");
240                        Matcher marr;
241                        String auxParamArr = param.trim();
242                        ArrayList<Integer> intList = new ArrayList<>();
243
244                        while (!auxParamArr.equals("")) {
245                            marr = parr.matcher(auxParamArr);
246                            if (!marr.matches()) {
247                                throw new SQLException("Invalid int array: "
248                                        + param);
249                            }
250
251                            intList.add(Integer.valueOf(marr.group(1)));
252                            auxParamArr = marr.group(2).trim();
253                        }
254                        objects.add(intList.toArray(new Integer[0]));
255                        types.add(int[].class);
256                    } else // it is an int
257                    {
258                        objects.add(Integer.valueOf(param));
259                        types.add(Integer.TYPE);
260                    }
261
262                    auxParamString = m.group(5).trim();
263                }
264            } else {
265                throw new SQLException("Syntax error");
266            }
267
268            m_methodArgs = objects.toArray(new Object[0]);
269            m_methodArgTypes = types.toArray(new Class[0]);
270        } catch (Exception e) {
271            throw new SQLException("Invalid method call: " + methodCall
272                    + ". Cause: " + e.toString());
273        }
274    }
275}