/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.translator.odata4; import java.io.InputStream; import java.net.URI; import java.sql.Blob; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.apache.olingo.client.api.serialization.ODataDeserializer; import org.apache.olingo.client.api.serialization.ODataDeserializerException; import org.apache.olingo.client.core.serialization.AtomDeserializer; import org.apache.olingo.client.core.serialization.JsonDeserializer; import org.apache.olingo.commons.api.data.Entity; import org.apache.olingo.commons.api.data.EntityCollection; import org.apache.olingo.commons.api.data.Link; import org.apache.olingo.commons.api.data.Property; import org.apache.olingo.commons.api.format.ContentType; import org.apache.olingo.commons.api.http.HttpStatusCode; import org.teiid.GeneratedKeys; import org.teiid.core.TeiidException; import org.teiid.language.Command; import org.teiid.metadata.RuntimeMetadata; import org.teiid.metadata.Table; import org.teiid.olingo.common.ODataTypeManager; import org.teiid.translator.DataNotAvailableException; import org.teiid.translator.ExecutionContext; import org.teiid.translator.TranslatorException; import org.teiid.translator.UpdateExecution; import org.teiid.translator.WSConnection; import org.teiid.translator.odata4.ODataUpdateExecution.EntityCollectionIterator.NotSupportedAccepts; import org.teiid.translator.odata4.ODataUpdateVisitor.OperationType; import org.teiid.translator.ws.BinaryWSProcedureExecution; public class ODataUpdateExecution extends BaseQueryExecution implements UpdateExecution { private ODataUpdateVisitor visitor; private Entity createdEntity; private AtomicInteger updateCount = new AtomicInteger(); public ODataUpdateExecution(Command command, ODataExecutionFactory translator, ExecutionContext executionContext, RuntimeMetadata metadata, WSConnection connection) throws TranslatorException { super(translator, executionContext, metadata, connection); this.visitor = new ODataUpdateVisitor(translator, metadata); this.visitor.visitNode(command); if (!this.visitor.exceptions.isEmpty()) { throw this.visitor.exceptions.get(0); } } @Override public void close() { } @Override public void cancel() throws TranslatorException { } @Override public void execute() throws TranslatorException { ODataUpdateQuery odataQuery = this.visitor.getODataQuery(); if (this.visitor.getOperationType() == OperationType.DELETE) { EntityCollectionIterator ecv = new JsonEntityCollectionIterator(odataQuery, "DELETE"); try { ecv.performUpdateQuery(); } catch (NotSupportedAccepts e) { ecv = new XMLEntityCollectionIterator(odataQuery, "DELETE"); try { ecv.performUpdateQuery(); } catch (NotSupportedAccepts e1) { throw new TranslatorException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17032)); } } } else if(this.visitor.getOperationType() == OperationType.UPDATE) { EntityCollectionIterator ecv = new JsonEntityCollectionIterator(odataQuery, odataQuery.getUpdateMethod()); try { ecv.performUpdateQuery(); } catch (NotSupportedAccepts e) { ecv = new XMLEntityCollectionIterator(odataQuery, odataQuery.getUpdateMethod()); try { ecv.performUpdateQuery(); } catch (NotSupportedAccepts e1) { throw new TranslatorException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID17032)); } } } else if (this.visitor.getOperationType() == OperationType.INSERT) { handleInsert(odataQuery); } } // TODO: this needs to be submitted as BATCH to be transactionally safe. abstract class EntityCollectionIterator { private ODataUpdateQuery odataQuery; private String method; private String uri; public EntityCollectionIterator(ODataUpdateQuery odataQuery, String method) throws TranslatorException { this.odataQuery = odataQuery; this.method = method; // fyi, the below is not immutable method this.uri = odataQuery.buildUpdateSelectionURL(""); } public void performUpdateQuery() throws TranslatorException, NotSupportedAccepts { while (this.uri != null) { // this needs to be full metadata, to get to the Entity edit URL, but note Olingo // currently does not support as of 4.0.0 BinaryWSProcedureExecution execution = invokeHTTP("GET",uri,null, headers()); if (execution.getResponseCode() == HttpStatusCode.OK.getStatusCode()) { EntityCollection entities = null; Blob blob = (Blob)execution.getOutputParameterValues().get(0); try { InputStream response = blob.getBinaryStream(); if (response != null){ ODataDeserializer serializer = getDeSerializer(); entities = serializer.toEntitySet(response).getPayload(); URI nextUri = entities.getNext(); if (nextUri != null) { this.uri = nextUri.toString(); } else { this.uri = null; } } } catch (ODataDeserializerException e) { throw new TranslatorException(e); } catch (SQLException e) { throw new TranslatorException(e); } if (entities != null && !entities.getEntities().isEmpty()) { for (Entity entity:entities.getEntities()) { onEntity(entity, method); } } } else { throw new NotSupportedAccepts(); } } } public void onEntity(Entity entity, String method) throws TranslatorException { Link editLink = entity.getEditLink(); Map<String, List<String>> updateHeaders = getDefaultUpdateHeaders(); if (entity.getETag() != null) { updateHeaders.put("If-Match", Arrays.asList(entity.getETag())); //$NON-NLS-1$ } BinaryWSProcedureExecution update = invokeHTTP( method, editLink.getHref(), method.equals("DELETE")?null:odataQuery.getPayload(entity), updateHeaders); if (HttpStatusCode.NO_CONTENT.getStatusCode() == update.getResponseCode()) { updateCount.incrementAndGet(); } else { throw buildError(update); } } public abstract Map<String, List<String>> headers(); public abstract ODataDeserializer getDeSerializer(); class NotSupportedAccepts extends Exception {} } class JsonEntityCollectionIterator extends EntityCollectionIterator { public JsonEntityCollectionIterator(ODataUpdateQuery odataQuery, String method) throws TranslatorException { super(odataQuery, method); } @Override public Map<String, List<String>> headers() { Map<String, List<String>> headers = new HashMap<String, List<String>>(); headers.put("Accept", Arrays.asList(ContentType.JSON_FULL_METADATA.toContentTypeString())); //$NON-NLS-1$ return headers; } @Override public ODataDeserializer getDeSerializer() { return new JsonDeserializer(false); } } class XMLEntityCollectionIterator extends EntityCollectionIterator { public XMLEntityCollectionIterator(ODataUpdateQuery odataQuery, String method) throws TranslatorException { super(odataQuery, method); } @Override public Map<String, List<String>> headers() { Map<String, List<String>> headers = new HashMap<String, List<String>>(); headers.put("Accept", Arrays.asList(ContentType.APPLICATION_ATOM_XML.toContentTypeString())); //$NON-NLS-1$ return headers; } @Override public ODataDeserializer getDeSerializer() { return new AtomDeserializer(); } } private Map<String, List<String>> getDefaultUpdateHeaders() { Map<String, List<String>> headers = new HashMap<String, List<String>>(); headers.put("Accept", Arrays.asList(ContentType.APPLICATION_JSON.toContentTypeString())); //$NON-NLS-1$ headers.put("Content-Type", Arrays.asList(ContentType.APPLICATION_JSON.toContentTypeString())); //$NON-NLS-1$ return headers; } private void handleInsert(ODataUpdateQuery odataQuery) throws TranslatorException { try { Map<String, List<String>> headers = getDefaultUpdateHeaders(); headers.put("Prefer", Arrays.asList("return=representation")); //$NON-NLS-1$ String uri = odataQuery.buildInsertURL(""); InputStream response = null; BinaryWSProcedureExecution execution = invokeHTTP(odataQuery.getInsertMethod(), uri, odataQuery.getPayload(null), headers); // 201 - the created entity returned if (HttpStatusCode.CREATED.getStatusCode() == execution.getResponseCode()) { Blob blob = (Blob)execution.getOutputParameterValues().get(0); response = blob.getBinaryStream(); } else if (HttpStatusCode.NO_CONTENT.getStatusCode() == execution.getResponseCode()) { // get Location header and get content String entityUri = (String)execution.getResponseHeader("Location"); if (entityUri != null) { // in the cases of property update there will be no Location header response = executeQuery("GET", entityUri, null, null, new HttpStatusCode[] {HttpStatusCode.OK}); } } else { throw buildError(execution); } if (response != null){ JsonDeserializer serializer = new JsonDeserializer(false); this.createdEntity = serializer.toEntity(response).getPayload(); } } catch (ODataDeserializerException e) { throw new TranslatorException(e); } catch (SQLException e) { throw new TranslatorException(e); } } @Override public int[] getUpdateCounts() throws DataNotAvailableException, TranslatorException { if (this.visitor.getOperationType() == OperationType.DELETE) { return new int[]{this.updateCount.get()}; } else if(this.visitor.getOperationType() == OperationType.UPDATE) { return new int[]{this.updateCount.get()}; } else if (this.visitor.getOperationType() == OperationType.INSERT) { if (this.createdEntity != null) { if (this.executionContext.getCommandContext().isReturnAutoGeneratedKeys()) { addAutoGeneretedKeys(this.visitor.getODataQuery().getRootDocument().getTable(), this.createdEntity); } } return new int[]{1}; } return new int[] {0}; } private void addAutoGeneretedKeys(Table table, Entity entity) throws TranslatorException { int cols = table.getPrimaryKey().getColumns().size(); Class<?>[] columnDataTypes = new Class<?>[cols]; String[] columnNames = new String[cols]; String[] odataTypes = new String[cols]; // this is typically expected to be an int/long, but we'll be general here. // we may eventual need the type logic off of the metadata importer for (int i = 0; i < cols; i++) { columnDataTypes[i] = table.getPrimaryKey().getColumns().get(i).getJavaType(); columnNames[i] = table.getPrimaryKey().getColumns().get(i).getName(); odataTypes[i] = ODataMetadataProcessor.getNativeType(table .getPrimaryKey().getColumns().get(i)); } GeneratedKeys generatedKeys = this.executionContext.getCommandContext() .returnGeneratedKeys(columnNames, columnDataTypes); List<Object> vals = new ArrayList<Object>(columnDataTypes.length); for (int i = 0; i < columnDataTypes.length; i++) { Property prop = entity.getProperty(columnNames[i]); Object value; try { value = ODataTypeManager.convertToTeiidRuntimeType(columnDataTypes[i], prop.getValue(), odataTypes[i]); } catch (TeiidException e) { throw new TranslatorException(e); } vals.add(value); } generatedKeys.addKey(vals); } }