/* * 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.olingo.service; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URI; import java.net.URISyntaxException; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; import java.sql.Date; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; 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.data.ValueType; import org.apache.olingo.commons.api.edm.EdmEntityType; import org.apache.olingo.commons.api.edm.EdmPrimitiveType; import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException; import org.apache.olingo.commons.core.edm.primitivetype.EdmBinary; import org.apache.olingo.commons.core.edm.primitivetype.EdmDate; import org.apache.olingo.commons.core.edm.primitivetype.EdmDateTimeOffset; import org.apache.olingo.commons.core.edm.primitivetype.EdmStream; import org.apache.olingo.commons.core.edm.primitivetype.EdmTimeOfDay; import org.apache.olingo.commons.core.edm.primitivetype.SingletonPrimitiveType; import org.apache.olingo.server.core.responses.EntityResponse; import org.teiid.core.TeiidProcessingException; import org.teiid.core.types.BlobType; import org.teiid.core.types.ClobType; import org.teiid.core.types.DataTypeManager; import org.teiid.core.types.Transform; import org.teiid.core.types.TransformationException; import org.teiid.core.util.ObjectConverterUtil; import org.teiid.logging.LogConstants; import org.teiid.logging.LogManager; import org.teiid.odata.api.QueryResponse; import org.teiid.olingo.ODataPlugin; import org.teiid.olingo.ProjectedColumn; import org.teiid.olingo.common.ODataTypeManager; import org.teiid.query.sql.symbol.Symbol; public class EntityCollectionResponse extends EntityCollection implements QueryResponse { interface Row { Object getObject(int column) throws SQLException; Object[] getArray(int columnIndex) throws SQLException; } private String nextToken; private DocumentNode documentNode; private String baseURL; private Map<String, Object> streams; private EntityCollectionResponse() { } public EntityCollectionResponse(String baseURL, DocumentNode resource) { this.baseURL = baseURL; this.documentNode = resource; } @Override public void addRow(ResultSet rs) throws SQLException { Entity entity = createEntity(rs, this.documentNode, this.baseURL, this); processExpands(asRow(rs), entity, this.documentNode); getEntities().add(entity); } private void processExpands(Row vals, Entity entity, DocumentNode node) throws SQLException { if (node.getExpands() == null || node.getExpands().isEmpty()) { return; } for (ExpandDocumentNode expandNode : node.getExpands()) { Object[] expandedVals = vals.getArray(expandNode.getColumnIndex()); if (expandedVals == null) { continue; } for (Object o : expandedVals) { Object[] expandedVal = (Object[])o; Entity expandEntity = createEntity(expandedVal, expandNode, this.baseURL, this); Link link = entity.getNavigationLink(expandNode.getNavigationName()); if (expandNode.isCollection()) { if (link.getInlineEntitySet() == null) { link.setInlineEntitySet(new EntityCollectionResponse()); } EntityCollectionResponse expandResponse = (EntityCollectionResponse)link.getInlineEntitySet(); boolean addEntity = expandResponse.processOptions( expandNode.getSkip(), expandNode.getTop(), expandEntity); if (addEntity) { link.getInlineEntitySet().getEntities().add(expandEntity); } } else { link.setInlineEntity(expandEntity); } processExpands(asRow(expandedVal), expandEntity, expandNode); } } } static Entity createEntity(final Object[] vals, DocumentNode node, String baseURL, EntityCollectionResponse response) throws SQLException { return createEntity(asRow(vals), node, baseURL, response); } static Entity createEntity(final ResultSet vals, DocumentNode node, String baseURL, EntityCollectionResponse response) throws SQLException { return createEntity(asRow(vals), node, baseURL, response); } static Entity createEntity(Row row, DocumentNode node, String baseURL, EntityCollectionResponse response) throws SQLException { List<ProjectedColumn> projected = node.getAllProjectedColumns(); EdmEntityType entityType = node.getEdmEntityType(); LinkedHashMap<String, Link> streamProperties = new LinkedHashMap<String, Link>(); Entity entity = new Entity(); entity.setType(entityType.getFullQualifiedName().getFullQualifiedNameAsString()); boolean allNulls = true; for (ProjectedColumn column: projected) { /* if (!column.isVisible()) { continue; }*/ String propertyName = Symbol.getShortName(column.getExpression()); Object value = row.getObject(column.getOrdinal()); if (value != null) { allNulls = false; } try { SingletonPrimitiveType type = (SingletonPrimitiveType) column.getEdmType(); if (type instanceof EdmStream) { buildStreamLink(streamProperties, value, propertyName); if (response != null) { //this will only be used for a stream response off of the first entity. In all other scenarios it will be ignored. response.setStream(propertyName, value); } } else { Property property = buildPropery(propertyName, type, column.getPrecision(), column.getScale(), column.isCollection(), value); entity.addProperty(property); } } catch (IOException e) { throw new SQLException(e); } catch (TeiidProcessingException e) { throw new SQLException(e); } } if (allNulls) { return null; } // Build the navigation and Stream Links try { String id = EntityResponse.buildLocation(baseURL, entity, entityType.getName(), entityType); entity.setId(new URI(id)); // build stream properties for (String name:streamProperties.keySet()) { Link link = streamProperties.get(name); link.setHref(id+"/"+name); entity.getMediaEditLinks().add(link); entity.addProperty(createPrimitive(name, EdmStream.getInstance(), new URI(link.getHref()))); } // build navigations for (String name:entityType.getNavigationPropertyNames()) { Link navLink = new Link(); navLink.setTitle(name); navLink.setHref(id+"/"+name); navLink.setRel("http://docs.oasis-open.org/odata/ns/related/"+name); entity.getNavigationLinks().add(navLink); Link assosiationLink = new Link(); assosiationLink.setTitle(name); assosiationLink.setHref(id+"/"+name+"/$ref"); assosiationLink.setRel("http://docs.oasis-open.org/odata/ns/relatedlinks/"+name); entity.getAssociationLinks().add(assosiationLink); } } catch (URISyntaxException e) { throw new SQLException(e); } catch (EdmPrimitiveTypeException e) { throw new SQLException(e); } return entity; } private static Row asRow(final ResultSet vals) { return new Row() { @Override public Object getObject(int column) throws SQLException { return vals.getObject(column); } @Override public Object[] getArray(int columnIndex) throws SQLException { Array array = vals.getArray(columnIndex); if (array == null) { return null; } return (Object[]) array.getArray(); } }; } private static Row asRow(final Object[] vals) { return new Row() { @Override public Object getObject(int column) throws SQLException { return vals[column - 1]; } @Override public Object[] getArray(int columnIndex) { return (Object[]) vals[columnIndex - 1]; } }; } void setStream(String propertyName, Object value) { if (this.streams == null) { this.streams = new HashMap<String, Object>(); } streams.put(propertyName, value); } public Object getStream(String propertyName) { if (this.streams == null) { return null; } return streams.get(propertyName); } private static void buildStreamLink(LinkedHashMap<String, Link> streamProperties, Object value, String propName) { if (value != null) { // read link Link streamLink = new Link(); streamLink.setTitle(propName); if (value instanceof SQLXML) { streamLink.setType("application/xml"); } else if (value instanceof Clob) { streamLink.setType("application/json"); } else if (value instanceof Blob) { streamLink.setType("application/octet-stream"); } streamLink.setRel("http://docs.oasis-open.org/odata/ns/mediaresource/"+propName); streamProperties.put(propName, streamLink); } } static Property buildPropery(String propName, SingletonPrimitiveType type, Integer precision, Integer scale, boolean isArray, Object value) throws TeiidProcessingException, SQLException, IOException { if (value instanceof Array) { value = ((Array) value).getArray(); int length = java.lang.reflect.Array.getLength(value); ArrayList<Object> values = new ArrayList<Object>(); for (int i = 0; i < length; i++) { Object o = java.lang.reflect.Array.get(value, i); if (o != null && o.getClass().isArray()) { throw new TeiidNotImplementedException(ODataPlugin.Event.TEIID16029, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16029, propName)); } Object p = getPropertyValue(type, precision, scale, isArray, o); values.add(p); } return createCollection(propName, type, values); } if (isArray) { ArrayList<Object> values = new ArrayList<Object>(); values.add(getPropertyValue(type, precision, scale, isArray, value)); return createCollection(propName, type, values); } return createPrimitive(propName, type, getPropertyValue(type, precision, scale, isArray, value)); } private static Property createPrimitive(final String name, EdmPrimitiveType type, final Object value) { return new Property(type.getFullQualifiedName().getFullQualifiedNameAsString(), name, ValueType.PRIMITIVE, value); } private static Property createCollection(final String name, EdmPrimitiveType type, final ArrayList<Object> values) { return new Property(type.getFullQualifiedName().getFullQualifiedNameAsString(), name, ValueType.COLLECTION_PRIMITIVE, values); } /* static Object getPropertyValue(SingletonPrimitiveType expectedType, boolean isArray, Object value, String invalidCharacterReplacement) throws TransformationException, SQLException, IOException { if (value == null) { return null; } Class<?> sourceType = DataTypeManager.getRuntimeType(value.getClass()); Class<?> targetType = DataTypeManager.getDataTypeClass(ODataTypeManager.teiidType(expectedType, isArray)); if (sourceType != targetType) { Transform t = DataTypeManager.getTransform(sourceType, targetType); if (t == null && BlobType.class == targetType) { if (sourceType == ClobType.class) { return ClobType.getString((Clob) value).getBytes(); } if (sourceType == SQLXML.class) { return ((SQLXML) value).getString().getBytes(); } } value = t != null ? t.transform(value, targetType) : value; value = replaceInvalidCharacters(expectedType, value, invalidCharacterReplacement); return value; } value = replaceInvalidCharacters(expectedType, value, invalidCharacterReplacement); return value; } */ static Object getPropertyValue(SingletonPrimitiveType expectedType, Integer precision, Integer scale, boolean isArray, Object value) throws TransformationException, SQLException, IOException { if (value == null) { return null; } value = getPropertyValueInternal(expectedType, isArray, value); if (value instanceof BigDecimal) { BigDecimal bigDecimalValue = (BigDecimal)value; //if precision is set, then try to set an appropriate scale to pass the facet check if (precision != null) { final int digits = bigDecimalValue.scale() >= 0 ? Math.max(bigDecimalValue.precision(), bigDecimalValue.scale()) : bigDecimalValue.precision() - bigDecimalValue.scale(); if (bigDecimalValue.scale() > (scale == null ? 0 : scale) || (digits > precision)) { bigDecimalValue = bigDecimalValue.setScale(Math.min(digits > precision ? bigDecimalValue.scale() - digits + precision : bigDecimalValue.scale(), scale == null ? 0 : scale), RoundingMode.HALF_UP); } } value = bigDecimalValue; } return value; } private static Object getPropertyValueInternal(SingletonPrimitiveType expectedType, boolean isArray, Object value) throws TransformationException, SQLException, IOException { Class<?> sourceType = DataTypeManager.getRuntimeType(value.getClass()); if (sourceType.isAssignableFrom(expectedType.getDefaultType())) { return value; } if (expectedType instanceof EdmDate && sourceType == Date.class) { return value; } else if (expectedType instanceof EdmDateTimeOffset && sourceType == Timestamp.class){ return value; } else if (expectedType instanceof EdmTimeOfDay && sourceType == Time.class){ return value; } else if (expectedType instanceof EdmBinary) { // there could be memory implications here, should have been modeled as EdmStream LogManager.logDetail(LogConstants.CTX_ODATA, "Possible OOM when inlining the stream based values"); //$NON-NLS-1$ if (sourceType == ClobType.class) { return ClobType.getString((Clob) value).getBytes(); } if (sourceType == SQLXML.class) { return ((SQLXML) value).getString().getBytes(); } if (sourceType == BlobType.class) { return ObjectConverterUtil.convertToByteArray(((Blob)value).getBinaryStream()); } if (value instanceof Serializable) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(value); oos.close(); bos.close(); return bos.toByteArray(); } } Class<?> targetType = DataTypeManager.getDataTypeClass(ODataTypeManager.teiidType(expectedType, isArray)); if (sourceType != targetType) { Transform t = DataTypeManager.getTransform(sourceType, targetType); value = t != null ? t.transform(value, targetType) : value; } return value; } @Override public long size() { return getEntities().size(); } @Override public void setCount(long count) { this.collectionCount = (int)count; } @Override public void setNextToken(String token) { this.nextToken = token; } @Override public String getNextToken() { return this.nextToken; } private int skipped = 0; private int topCount = 0; private int collectionCount = 0; private boolean processOptions(int skip, int top, Entity expandEntity) { this.collectionCount++; if (skip > 0 && this.skipped < skip) { this.skipped++; return false; } if (top > -1 ) { if (this.topCount < top) { this.topCount++; return true; } return false; } return true; } @Override public Integer getCount() { return this.collectionCount; } }