/*
* 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.web;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.ref.SoftReference;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.olingo.commons.api.format.ContentType;
import org.apache.olingo.commons.api.http.HttpHeader;
import org.apache.olingo.server.api.ODataHttpHandler;
import org.teiid.core.TeiidProcessingException;
import org.teiid.core.util.LRUCache;
import org.teiid.deployers.CompositeVDB;
import org.teiid.deployers.VDBLifeCycleListener;
import org.teiid.jdbc.ConnectionImpl;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.odata.api.Client;
import org.teiid.olingo.ODataPlugin;
import org.teiid.olingo.service.LocalClient;
import org.teiid.olingo.service.OlingoBridge;
import org.teiid.transport.LocalServerConnection;
import org.teiid.vdb.runtime.VDBKey;
public class ODataFilter implements Filter, VDBLifeCycleListener {
protected String proxyBaseURI;
protected Properties initProperties;
protected Map<VDBKey, SoftReference<OlingoBridge>> contextMap = Collections
.synchronizedMap(new LRUCache<VDBKey, SoftReference<OlingoBridge>>());
private volatile boolean listenerRegistered = false;
@Override
public void init(FilterConfig config) throws ServletException {
// handle proxy-uri in the case of cloud environments
String proxyURI = config.getInitParameter("proxy-base-uri"); //$NON-NLS-1$
if (proxyURI != null && proxyURI.startsWith("${") && proxyURI.endsWith("}")) { //$NON-NLS-1$ //$NON-NLS-2$
proxyURI = proxyURI.substring(2, proxyURI.length() - 1);
proxyURI = System.getProperty(proxyURI);
}
if (proxyURI != null) {
this.proxyBaseURI = proxyURI;
}
Properties props = new Properties();
Enumeration<String> names = config.getServletContext().getInitParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
props.setProperty(name, config.getServletContext().getInitParameter(name));
}
names = config.getInitParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
props.setProperty(name, config.getInitParameter(name));
}
this.initProperties = props;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
internalDoFilter(request, response, chain);
} catch (TeiidProcessingException e) {
//TODO: use engine style logic to determine if the stack should be logged
LogManager.logWarning(LogConstants.CTX_ODATA, e, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16047, e.getMessage()));
HttpServletResponse httpResponse = (HttpServletResponse)response;
writeError(request, e, httpResponse, 404);
}
}
static void writeError(ServletRequest request, TeiidProcessingException e,
HttpServletResponse httpResponse, int statusCode) throws IOException {
httpResponse.setStatus(statusCode);
ContentType contentType = ContentType.parse(request.getContentType());
PrintWriter writer = httpResponse.getWriter();
String code = e.getCode()==null?"":e.getCode(); //$NON-NLS-1$
String message = e.getMessage()==null?"":e.getMessage(); //$NON-NLS-1$
if (contentType == null || contentType.isCompatible(ContentType.APPLICATION_JSON)) {
httpResponse.setHeader(HttpHeader.CONTENT_TYPE, ContentType.APPLICATION_JSON.toContentTypeString());
writer.write("{ \"error\": { \"code\": \""+StringEscapeUtils.escapeJson(code)+"\", \"message\": \""+StringEscapeUtils.escapeJson(message)+"\" } }"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
} else {
httpResponse.setHeader(HttpHeader.CONTENT_TYPE, ContentType.APPLICATION_XML.toContentTypeString());
writer.write("<m:error xmlns:m=\"http://docs.oasis-open.org/odata/ns/metadata\"><m:code>"+StringEscapeUtils.escapeXml10(code)+"</m:code><m:message>"+StringEscapeUtils.escapeXml10(message)+"</m:message></m:error>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
writer.close();
}
public void internalDoFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException, TeiidProcessingException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String proxyURI = this.proxyBaseURI;
if (proxyURI != null) {
httpRequest = new ProxyHttpServletRequest(httpRequest, proxyURI);
}
VDBKey key = null;
String vdbName = null;
String version = null;
String modelName = null;
String uri = ((HttpServletRequest) request).getRequestURI().toString();
String fullURL = ((HttpServletRequest) request).getRequestURL().toString();
if (uri.startsWith("/odata4/static/") || uri.startsWith("/odata4/keycloak/")){ //$NON-NLS-1$ //$NON-NLS-2$
chain.doFilter(httpRequest, response);
return;
}
String contextPath = httpRequest.getContextPath();
String baseURI = fullURL.substring(0, fullURL.indexOf(contextPath));
int endIdx = uri.indexOf('/', contextPath.length() + 1);
int beginIdx = contextPath.length() + 1;
if (contextPath.equals("/odata4")) { //$NON-NLS-1$
if (endIdx == -1) {
throw new TeiidProcessingException(ODataPlugin.Event.TEIID16020, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16020));
}
baseURI = baseURI+"/odata4"; //$NON-NLS-1$
vdbName = uri.substring(beginIdx, endIdx);
int modelIdx = uri.indexOf('/', endIdx + 1);
if (modelIdx == -1) {
modelName = uri.substring(endIdx + 1).trim();
if (modelName.isEmpty()) {
throw new TeiidProcessingException(ODataPlugin.Event.TEIID16019, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16019));
}
} else {
modelName = uri.substring(endIdx + 1, modelIdx);
}
contextPath = contextPath + "/" + vdbName + "/" + modelName; //$NON-NLS-1$ //$NON-NLS-2$
vdbName = vdbName.trim();
if (vdbName.isEmpty()) {
throw new TeiidProcessingException(ODataPlugin.Event.TEIID16008, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16008));
}
} else {
if (this.initProperties.getProperty("vdb-name") == null) { //$NON-NLS-1$
throw new TeiidProcessingException(ODataPlugin.Event.TEIID16018, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16018));
}
vdbName = this.initProperties.getProperty("vdb-name"); //$NON-NLS-1$
version = this.initProperties.getProperty("vdb-version"); //$NON-NLS-1$
if (endIdx == -1) {
modelName = uri.substring(beginIdx).trim();
if (modelName.isEmpty()) {
throw new TeiidProcessingException(ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16021));
}
} else {
modelName = uri.substring(beginIdx, endIdx);
}
contextPath = contextPath + "/" + modelName; //$NON-NLS-1$
}
ContextAwareHttpSerlvetRequest contextAwareRequest = new ContextAwareHttpSerlvetRequest(httpRequest);
contextAwareRequest.setContextPath(contextPath);
httpRequest = contextAwareRequest;
key = new VDBKey(vdbName, version);
if (key.isAtMost()) {
if (key.getVersion() != null) {
throw new TeiidProcessingException(ODataPlugin.Event.TEIID16044, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16044, key));
}
key = new VDBKey(vdbName, "1"); //$NON-NLS-1$ //legacy behavior, default to version 1
}
SoftReference<OlingoBridge> ref = this.contextMap.get(key);
OlingoBridge context = null;
if (ref != null) {
context = ref.get();
}
if (context == null) {
context = new OlingoBridge();
ref = new SoftReference<OlingoBridge>(context);
this.contextMap.put(key, ref);
}
Client client = buildClient(key.getName(), key.getVersion(), this.initProperties);
try {
Connection connection = client.open();
registerVDBListener(client, connection);
ODataHttpHandler handler = context.getHandler(baseURI, client, modelName);
httpRequest.setAttribute(ODataHttpHandler.class.getName(), handler);
httpRequest.setAttribute(Client.class.getName(), client);
chain.doFilter(httpRequest, response);
} catch(SQLException e) {
throw new TeiidProcessingException(e);
} finally {
try {
client.close();
} catch (SQLException e) {
//ignore
}
}
}
private void registerVDBListener(Client client, Connection conn) {
if (!this.listenerRegistered) {
synchronized (this) {
if (!this.listenerRegistered) {
if (client instanceof LocalClient) {
try {
ConnectionImpl connection = (ConnectionImpl)conn;
LocalServerConnection lsc = (LocalServerConnection) connection.getServerConnection();
lsc.addListener(this);
this.listenerRegistered = true;
} catch (SQLException e) {
LogManager.logWarning(LogConstants.CTX_ODATA, ODataPlugin.Util.gs(ODataPlugin.Event.TEIID16014));
}
}
}
}
}
}
public Client buildClient(String vdbName, String version, Properties props) {
return new LocalClient(vdbName, version, props);
}
@Override
public void destroy() {
this.contextMap.clear();
}
@Override
public void removed(String name, CompositeVDB vdb) {
this.contextMap.remove(vdb.getVDBKey());
}
@Override
public void finishedDeployment(String name, CompositeVDB vdb) {
this.contextMap.remove(vdb.getVDBKey());
}
@Override
public void beforeRemove(String name, CompositeVDB vdb) {
}
@Override
public void added(String name, CompositeVDB vdb) {
}
}