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 *   Chapman Flack
012 */
013package org.postgresql.pljava.example.annotation;
014
015import java.sql.Connection;
016import static java.sql.DriverManager.getConnection;
017import java.sql.SQLData;
018import java.sql.SQLDataException;
019import java.sql.SQLException;
020import java.sql.SQLInput;
021import java.sql.SQLOutput;
022import java.sql.Statement;
023
024import org.postgresql.pljava.annotation.Cast;
025import org.postgresql.pljava.annotation.Function;
026import org.postgresql.pljava.annotation.SQLAction;
027import org.postgresql.pljava.annotation.SQLType;
028import org.postgresql.pljava.annotation.BaseUDT;
029
030import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE;
031import static
032    org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL;
033
034/**
035 * An integer-like data type accepting a type modifier: IntWithMod(even) or
036 * IntWithMod(odd).
037 *<p>
038 * Support for type modifiers in PL/Java is only partial so far. It does not
039 * yet honor typmods passed to the input/receive functions ... but it may be
040 * that only COPY operations require that. Most uses of types with modifiers
041 * seem to pass -1 when first constructing the value, then use a typmod-
042 * application cast, and all of that can be done in PL/Java already.
043 *<p>
044 * Serving suggestion:
045 *<pre>
046 * CREATE TABLE evod (
047 *   ev javatest.IntWithMod(even),
048 *   od javatest.IntWithMod(odd)
049 * );
050 * INSERT INTO evod ( ev, od ) VALUES ( '4', '7' );
051 *</pre>
052 *<p>
053 * Of course this example more or less duplicates what you could do in two lines
054 * with CREATE DOMAIN. But it is enough to illustrate the process.
055 */
056@SQLAction(requires="IntWithMod modCast",
057    install={
058        "SELECT CAST('42' AS javatest.IntWithMod(even))"
059    }
060)
061@BaseUDT(schema="javatest", provides="IntWithMod type",
062    typeModifierInput="javatest.intwithmod_typmodin",
063    typeModifierOutput="javatest.intwithmod_typmodout",
064    like="pg_catalog.int4")
065public class IntWithMod implements SQLData {
066
067    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
068    public static IntWithMod parse(String input, String typeName)
069        throws SQLException {
070        try {
071            int v = Integer.parseInt(input);
072            IntWithMod o = new IntWithMod();
073            o.m_value = v;
074            o.m_typeName = typeName;
075            return o;
076        }
077        catch ( NumberFormatException e ) {
078            throw new SQLDataException("invalid IntWithMod value", "22P02", e);
079        }
080    }
081
082    private int m_value;
083
084    private String m_typeName;
085
086    public IntWithMod() {
087    }
088
089    @Override
090    public String getSQLTypeName() {
091        return m_typeName;
092    }
093
094    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
095    @Override
096    public void readSQL(SQLInput stream, String typeName) throws SQLException {
097        m_value = stream.readInt();
098        m_typeName = typeName;
099
100        /*
101         * This bit here is completely extraneous to the IntWithMod example, but
102         * simply included to verify that PL/Java works right if a UDT's readSQL
103         * method ends up invoking some other PL/Java function.
104         */
105        try (
106            Connection c = getConnection("jdbc:default:connection");
107            Statement s = c.createStatement();
108        )
109        {
110            s.execute("SELECT javatest.java_addone(42)");
111        }
112    }
113
114    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
115    @Override
116    public String toString() {
117        return String.valueOf(m_value);
118    }
119
120    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
121    @Override
122    public void writeSQL(SQLOutput stream) throws SQLException {
123        stream.writeInt(m_value);
124    }
125
126    /**
127     * Type modifier input function for IntWithMod type: accepts
128     * "even" or "odd". The modifier value is 0 for even or 1 for odd.
129     */
130    @Function(schema="javatest", name="intwithmod_typmodin",
131        effects=IMMUTABLE, onNullInput=RETURNS_NULL)
132    public static int modIn(@SQLType("pg_catalog.cstring[]") String[] toks)
133        throws SQLException {
134        if ( 1 != toks.length )
135            throw new SQLDataException(
136                "only one type modifier allowed for IntWithMod", "22023");
137        if ( "even".equalsIgnoreCase(toks[0]) )
138            return 0;
139        if ( "odd".equalsIgnoreCase(toks[0]) )
140            return 1;
141        throw new SQLDataException(
142            "modifier for IntWithMod must be \"even\" or \"odd\"", "22023");
143    }
144
145    /**
146     * Type modifier output function for IntWithMod type.
147     */
148    @Function(schema="javatest", name="intwithmod_typmodout",
149        type="pg_catalog.cstring",
150        effects=IMMUTABLE, onNullInput=RETURNS_NULL)
151    public static String modOut(int mod) throws SQLException {
152        switch ( mod ) {
153            case 0: return "(even)";
154            case 1: return "(odd)";
155            default:
156                throw new SQLException("impossible IntWithMod typmod: " + mod);
157        }
158    }
159
160    /**
161     * Function backing the type-modifier application cast for IntWithMod type.
162     */
163    @Function(schema="javatest", name="intwithmod_typmodapply",
164        effects=IMMUTABLE, onNullInput=RETURNS_NULL)
165    @Cast(comment=
166        "Cast that applies/verifies the type modifier on an IntWithMod.",
167        provides="IntWithMod modCast")
168    public static IntWithMod modApply(IntWithMod iwm, int mod, boolean explicit)
169        throws SQLException
170    {
171        if ( -1 == mod )
172            return iwm;
173        if ( (iwm.m_value & 1) != mod )
174            throw new SQLDataException(
175                "invalid value " + iwm + " for " +
176                iwm.getSQLTypeName() + modOut(mod), "22000");
177        iwm.m_typeName += modOut(mod);
178        return iwm;
179    }
180}