Новые источники данных для Teiid, часть 2: пишем транслятор

в 21:44, , рубрики: java, jboss, teiid, метки: , ,

В предыдущей части мы коротко рассмотрели способ описания источника данных при помощи DDL. Но что делать, если источник использует специфический протокол? Если прямого отображения данных недостаточно? Если мы хотим добавить предобработку данных или встроенную процедуру? Выход есть: написать транслятор.

В общем случае транслятор состоит из 4 частей (классов). Это — наследник класса ExecutionFactory и реализации интерфейсов ResultSetExecution, ProcedureExecution и UpdateExecution; кроме этого, штатные трансляторы Teiid для удобства выделяют в отдельный класс обработчик метаданных (metadata processor), хотя он никакого интерфейса не реализует и ни от чего не наследуется. Рассмотрим всё по порядку.

Составные части транслятора

  • ExecutionFactory — отправная точка транслятора, место, где задаётся его имя и описание, его свойства (properties), описываются конструкции SQL, которые он поддерживает, формируются метаданные (т.е. создаются описания таблиц и процедур, которые реализует транслятор), а также, если нужно, создаются экземпляры вышеперечисленных интерфейсов, непосредственно выполняющие обработку запросов SELECT, INSERT/UPDATE/DELETE и вызовов встроенных процедур.
  • ResultSetExecution — интерфейс, реализации которого служат для обработки запросов SELECT. Реализации могут отсутствовать в трансляторе.
  • ProcedureExecution — интерфейс, реализации которого служат для обработки вызовов встроенных процедур. Реализации могут отсутствовать в трансляторе.
  • UpdateExecution — интерфейс, реализации которого служат для обработки запросов INSERT/UPDATE/DELETE. Реализации могут отсутствовать в трансляторе.

Дальше мы будем продвигаться по этим пунктам.

Базовые настройки

Для того, чтобы Teiid увидел наш транслятор, нам необходимо, чтобы в результирующем jar'е существовал файл META-INFservicesorg.teiid.translator.ExecutionFactory в который мы добавим строку:

ru.habrahabr.HabrExecutionFactory

Кроме того, для JBoss 7 в файл %JBOSS_HOME%/standalone/configuration/standalone.xml (или [..]domain[..]) нужно добавить такое:

<subsystem xmlns="urn:jboss:domain:teiid:1.0">
[..]
    <translator name="habrahabr" module="ru.habrahabr"/>
[..]

Здесь мы указываем серверу, в каком модуле находится наш транслятор. Понятно, что jar с нашим транслятором нужно поместить в модуль ru.habrahabr.

И здесь же, как и в прошлой статье, нам нужно прописать источник данных для транслятора.

<subsystem xmlns="urn:jboss:domain:resource-adapters:1.0">
[...]
    <resource-adapters>
        <resource-adapter>
            <archive>teiid-connector-ws.rar</archive>
            <transaction-support>NoTransaction</transaction-support>
            <connection-definitions>
                <connection-definition class-name="org.teiid.resource.adapter.ws.WSManagedConnectionFactory" jndi-name="java:/habrDS" enabled="true" use-java-context="true" pool-name="habr-ds">
                      <config-property name="EndPoint">http://habrahabr.ru/api/profile/</config-property>
                </connection-definition>
            </connection-definitions>
        </resource-adapter>
    </resource-adapters>
[...]

Описание модели

В самом простом случае, когда не задаются параметры ни для самой модели, ни для транслятора, описание модели в файле vdb будет выглядеть так:

    <model name="habr">
        <source name="habr" translator-name="habrahabr" connection-jndi-name="java:/habrDS"/>
    </model>

Всё.

Если необходимо задать свойства транслятора, то придётся описать транслятор-наследник:

    <model name="habr">
        <source name="habr" translator-name="habrahabr2" connection-jndi-name="java:/habrDS"/>
    </model>
    <translator name="habrahabr2" type="habrahabr">
        <property name="defaultUser" value="elfuegobiz"/>
    </translator>

А если нужно ещё и указать параметры для сборщика метаданных, то добавится ещё одна строчка:

    <model name="habr">
        <source name="habr" translator-name="habrahabr2" connection-jndi-name="java:/habrDS"/>
        <property name="importer.convertToUppercase" value="true"/>
    </model>
    <translator name="habrahabr2" type="habrahabr">
        <property name="defaultUser" value="elfuegobiz"/>
    </translator>

ExecutionFactory

Здесь мы можем сделать вот что:

  1. Задать имя и описание транслятора.
  2. Описать свойства транслятора.
  3. Описать конструкции SQL, которые мы будем поддерживать.
  4. Создать метаданные поддерживаемых таблиц и процедур.
  5. Создать экземпляры классов для обработки запросов.

Итак, поехали.

Имя и описание транслятора

package ru.habrahabr;

import javax.resource.cci.ConnectionFactory;

import org.teiid.translator.ExecutionFactory;
import org.teiid.translator.Translator;
import org.teiid.translator.WSConnection;

@Translator(name = "habrahabr", description = "A translator for Habrahabr API")
public class HabrExecutionFactory extends ExecutionFactory<ConnectionFactory, WSConnection> {
}

Аннотация Translator помечает класс как класс транслятора, в параметре name задаётся имя, под которым он будет виден в системе, а description — произвольное текстовое описание. Параметры при наследовании мы задаём исходя из того, что мы используем штатный WS connector Teiid.

Свойства транслятора

Выше, в главе «Описание модели» мы указали для транслятора значение свойства defaultUser. Теперь реализуем его в коде транслятора.

import org.teiid.translator.TranslatorProperty;

[..]

	private String defaultUser;

	@TranslatorProperty(description="Default user name",
			display="Default user name to use for table queries", required=true)
	public String getDefaultUser() {
		return defaultUser;
	}

	public void setDefaultUser(String defaultUser) {
		this.defaultUser = defaultUser;
	}

Аннотация TranslatorProperty к геттеру задаёт краткое и развёрнутое описание свойства. Если данное свойство обязательно, можно добавить к аннотации параметр required=true.

При создании экземпляра транслятора Teiid автоматически заполнит это поле значением из описания транслятора в vdb файле.

Поддержка конструкций SQL

У класса ExecutionFactory есть много методов с маской имени supports*, которые возвращают значение boolean, говорящее о том, поддерживает транслятор данную возможность или нет. Возможности, которые не поддерживает транслятор, Teiid реализует собственными средствами. Например, если транслятор не умеет обрабатывать функцию-агрегат sum(), то Teiid запросит у транслятора данные, а потом сам посчитает сумму.

Для изменения этих установок почти все из них необходимо перекрывать, лишь для нескольких есть соответствующие сеттеры, которые можно вызвать в конструкторе. Например, если мы хотим реализовать поддержку запросов count(*) и конструкции limit <число>, то мы перекроем соответствующие методы и будем в них возвращать true:

	@Override
	public boolean supportsAggregatesCountStar() {
		return true;
	}

	@Override
	public boolean supportsRowLimit() {
		return true;
	}

Эти все возможности есть смысл реализовывать, если сам источник данных имеет API для их реализации. Однако, наш транслятор будет совсем простым — под стать источнику, более сложный транслятор мы, возможно, напишем в следующей статье, если аудитория выкажет интерес к теме вообще.

Метаданные таблиц и процедур

Для описания реализуемых в трансляторе таблиц и процедур в классе ExecutionFactory есть специальный метод getMetadata(MetadataFactory metadataFactory, WSConnection conn), который необходимо перекрыть и реализовать. На вход нам приходит экземпляр класса MetadataFactory, который имеет методы для создания таблиц, процедур и многого другого, а также объект уже установленного соединения к нашему web-сервису: если нам нужно запросить данные о, например, количестве и именах таблиц у нашего источника. В нашем же случае мы заранее знаем, что мы хотим реализовать:

	@Override
	public void getMetadata(MetadataFactory metadataFactory, WSConnection conn)
			throws TranslatorException {
		Table table = metadataFactory.addTable("habr");
		metadataFactory.addColumn("login", DefaultDataTypes.STRING, table);
		metadataFactory.addColumn("karma", DefaultDataTypes.FLOAT, table);
		metadataFactory.addColumn("rating", DefaultDataTypes.FLOAT, table);
		metadataFactory.addColumn("ratingposition", DefaultDataTypes.LONG, table);

		Procedure proc = metadataFactory.addProcedure("getHabr");
		metadataFactory.addProcedureParameter("username",
				TypeFacility.RUNTIME_NAMES.STRING, Type.In, proc);
		metadataFactory.addProcedureParameter("ratingposition",
				TypeFacility.RUNTIME_NAMES.LONG, Type.ReturnValue, proc);
		metadataFactory.addProcedureParameter("rating",
				TypeFacility.RUNTIME_NAMES.FLOAT, Type.ReturnValue, proc);
		metadataFactory.addProcedureParameter("karma",
				TypeFacility.RUNTIME_NAMES.FLOAT, Type.ReturnValue, proc);
		metadataFactory.addProcedureParameter("login",
				TypeFacility.RUNTIME_NAMES.STRING, Type.ReturnValue, proc);
	}

Мы описали таблицу с четырьмя полями и процедуру с одним параметром и четырьмя возвращаемыми значениями. Т.к. мы не можем передать параметр в таблицу, таблица будет возвращать данные для defaultUser — параметра, который мы прописали транслятору при настройке. С процедурой всё проще, здесь мы можем и передать параметр, и получить в ответ несколько значений. Замечу в скобках, что процедуры могут возвращать не только фиксированное количество возвращаемых значений, а и целую таблицу, как и ResultSetExecution: для этого при описании параметров нужно использовать тип Type.Result, а также реализовать в обработчике метод next().

Обработчики

Для обработки вызовов нашей процедуры и запросов к нашей таблице мы должны предоставить обработчики. Например, вот таким образом.

	public ResultSetExecution createResultSetExecution(QueryExpression command,
			ExecutionContext executionContext, RuntimeMetadata metadata,
			WSConnection connection) throws TranslatorException {
		return new HabrResultSetExecution((Select) command, connection);
	}

	public ProcedureExecution createProcedureExecution(Call command,
			ExecutionContext executionContext, RuntimeMetadata metadata,
			WSConnection connection) throws TranslatorException {
		return new HabrProcedureExecution(command, connection);
	}

Если мы имеем несколько таблиц или процедур и для разных хотим предоставлять разные обработчики, мы можем в этих методах делать проверки:

	public ResultSetExecution createResultSetExecution(QueryExpression command,
			ExecutionContext executionContext, RuntimeMetadata metadata,
			WSConnection connection) throws TranslatorException {
		String tableName = ((NamedTable) command.getProjectedQuery().getFrom().get(0)).
			getMetadataObject().getName();
		if ("habr".equalsIgnoreCase(tableName))
			return new HabrResultSetExecution((Select) command, connection);
		if ("hrenabr".equalsIgnoreCase(tableName))
			return new HrenabrResultSetExecution((Select) command, connection);
		return null;
	}

	public ProcedureExecution createProcedureExecution(Call command,
			ExecutionContext executionContext, RuntimeMetadata metadata,
			WSConnection connection) throws TranslatorException {
		if ("getHabr".equalsIgnoreCase(command.getProcedureName()))
			return new HabrProcedureExecution(command, connection);
		if ("getHrenabr".equalsIgnoreCase(command.getProcedureName()))
			return new HrenabrProcedureExecution(command, connection);
		return null;
	}

Но в нашем трансляторе мы так делать не станем: незачем.

Ах, да… маленькая уличная магия

Внимательный читатель наверняка заметил, что до сих пор мы нигде не использовали свойство модели importer.convertToUppercase, которые мы прописали в конфигурации. Код Teiid использует эту приятную возможность, используем её и мы:

[..]
	private boolean convertToUppercase;

	public boolean isConvertToUppercase() {
		return convertToUppercase;
	}

	public void setConvertToUppercase(boolean convertToUppercase) {
		this.convertToUppercase = convertToUppercase;
	}
[..]
	@Override
	public void getMetadata(MetadataFactory metadataFactory, WSConnection conn)
			throws TranslatorException {
[..]
		PropertiesUtils.setBeanProperties(this, metadataFactory.getImportProperties(), "importer");
	}

Как нетрудно догадаться по коду, метод PropertiesUtils.setBeanProperties умеет фильтровать по префиксу свойства, которые из конфигурации модели передаются в MetadataFactory и доступны через metadataFactory.getImportProperties(), и заполнять ими указанный POJO с учётом преобразования типов и проверкой наличия свойств вообще. Т.о. мы бесплатно получаем удобный способ передачи в код конфигурационных параметров.

ResultSetExecution

Для начала напишем простой метод, который будет парсить входные данные и формировать строку таблицы для вывода, и добавим его в наш класс HabrExecutionFactory.
Также он учитывает значение нашего свойства convertToUppercase и, если оно установлено в true, логин преобразуется в заглавные буквы.

	protected List<Object> extractResult(DataSource dataSource) throws TranslatorException {
		List<Object> results = new ArrayList<Object>();
		try {
			DocumentBuilderFactory xmlFact = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder;
			builder = xmlFact.newDocumentBuilder();
			Document doc = builder.parse(dataSource.getInputStream());
			dataSource.getInputStream().close();
			final XPath xpath = XPathFactory.newInstance().newXPath();
			Node node = (Node) xpath.compile("/habrauser/login").evaluate(doc, XPathConstants.NODE);
			String login = node.getTextContent();
			if (convertToUppercase)
				login = login.toUpperCase();
			results.add(login);
			node = (Node) xpath.compile("/habrauser/karma").evaluate(doc, XPathConstants.NODE);
			results.add(Float.valueOf(node.getTextContent()));
			node = (Node) xpath.compile("/habrauser/rating").evaluate(doc, XPathConstants.NODE);
			results.add(Float.valueOf(node.getTextContent()));
			node = (Node) xpath.compile("/habrauser/ratingPosition").evaluate(doc, XPathConstants.NODE);
			results.add(Long.valueOf(node.getTextContent()));
		} catch (Exception e) {
			throw new TranslatorException(e);
		}
		return results;
	}

Теперь можно написать класс HabrResultSetExecution:

	public class HabrResultSetExecution implements ResultSetExecution {
		private final WSConnection conn;
		private boolean closed;
		private DataSource dataSource;

		public HabrResultSetExecution(Select query, WSConnection conn) {
			this.conn = conn;
		}

		@Override
		public void execute() throws TranslatorException {
			closed = false;
			try {
				Dispatch<DataSource> dispatch = conn.createDispatch(HTTPBinding.HTTP_BINDING,
						defaultUser, DataSource.class, Mode.MESSAGE);
				dispatch.getRequestContext().put(MessageContext.HTTP_REQUEST_METHOD, "GET");
				dataSource = dispatch.invoke(null);
			} catch (Exception e) {
				throw new TranslatorException(e);
			}
		}

		@Override
		public List<?> next() throws TranslatorException,
				DataNotAvailableException {
			if (closed)
				return null;
			closed = true;
			return extractResult(dataSource);
		}

		@Override
		public void close() {
			closed = true;
		}

		@Override
		public void cancel() throws TranslatorException {
			closed = true;
		}
	}

Метод execute() вызывается системой для выполнения запроса. В нашей реализации он выполняет запрос к сервису, пользуясь переданным ему соединением, и запоминает в поле экземпляр DataSource. После этого система вызывает метод next() до тех пор, пока он не вернёт null. В первый вызов метод передаёт сохранённый DataSource в метод extractResult(), который и выполняет всю работу, а также генерирует строку данных: List<?>, каждый из элементов которого соответствует полю строки БД. Т.к. у нас есть только одна строка данных, на второй и последующие вызовы мы и возвращаем null.

ProcedureExecution

Этот класс работает сходным образом. Есть только два различия:

  • метод execute() использует для вызова сервиса имя пользователя, переданное в параметр процедуре
  • вместо метода next() данные системой выбираются через метод getOutputParameterValues() (который вызывается только один раз на вызов); метод next(), как я уже раньше упоминал, может вызываться, если процедура умеет возвращать result set

	public class HabrProcedureExecution implements ProcedureExecution {

		private final Call procedure;
		private final WSConnection conn;
		private DataSource dataSource;

		public HabrProcedureExecution(Call procedure, WSConnection conn) {
			this.procedure = procedure;
			this.conn = conn;
		}

		@Override
		public void execute() throws TranslatorException {
			List<Argument> arguments = this.procedure.getArguments();

			String username = (String) arguments.get(0).getArgumentValue().getValue();
			try {
				Dispatch<DataSource> dispatch = conn.createDispatch(HTTPBinding.HTTP_BINDING,
						username, DataSource.class, Mode.MESSAGE);
				dispatch.getRequestContext().put(MessageContext.HTTP_REQUEST_METHOD, "GET");
				dataSource = dispatch.invoke(null);
			} catch (Exception e) {
				throw new TranslatorException(e);
			}
		}

		@Override
		public List<?> getOutputParameterValues() throws TranslatorException {
			return extractResult(dataSource);
		}

		@Override
		public List<?> next() throws TranslatorException, DataNotAvailableException {
			return null;
		}

		@Override
		public void close() {
		}

		@Override
		public void cancel() throws TranslatorException {
		}

	}

UpdateExecution

Этот обработчик мы, возможно, реализуем в следующей статье, если аудитория выкажет интерес к теме вообще.

Заключение

Вот мы и добрались до этого момента. Теперь нам осталось сделать только две вещи:

  1. выполнить вот эти запросы:
    select * from habr.habr;
    select w.* from (call habr.getHabr(username=>'elfuegobiz')) w;
    
  2. сказать: было клёво =)

Автор: elfuegobiz

Поделиться

* - обязательные к заполнению поля