/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package de.unioninvestment.eai.portal.portlet.crud.scripting.domain.container.rest; import static java.util.Collections.unmodifiableList; import groovy.lang.Closure; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.DefaultHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vaadin.data.Container.Filter; import de.unioninvestment.eai.portal.portlet.crud.config.GroovyScript; import de.unioninvestment.eai.portal.portlet.crud.config.ReSTAttributeConfig; import de.unioninvestment.eai.portal.portlet.crud.config.ReSTChangeConfig; import de.unioninvestment.eai.portal.portlet.crud.config.ReSTChangeMethodConfig; import de.unioninvestment.eai.portal.portlet.crud.config.ReSTContainerConfig; import de.unioninvestment.eai.portal.portlet.crud.config.ReSTFormatConfig; import de.unioninvestment.eai.portal.portlet.crud.domain.exception.BusinessException; import de.unioninvestment.eai.portal.portlet.crud.domain.exception.InvalidConfigurationException; import de.unioninvestment.eai.portal.portlet.crud.domain.exception.TechnicalCrudPortletException; import de.unioninvestment.eai.portal.portlet.crud.domain.model.GenericContainerRow; import de.unioninvestment.eai.portal.portlet.crud.domain.model.GenericContainerRowId; import de.unioninvestment.eai.portal.portlet.crud.domain.model.ReSTContainer; import de.unioninvestment.eai.portal.portlet.crud.domain.model.ReSTDelegate; import de.unioninvestment.eai.portal.portlet.crud.domain.model.authentication.Realm; import de.unioninvestment.eai.portal.portlet.crud.domain.support.AuditLogger; import de.unioninvestment.eai.portal.portlet.crud.scripting.model.ScriptRow; import de.unioninvestment.eai.portal.support.scripting.ScriptBuilder; import de.unioninvestment.eai.portal.support.vaadin.container.Column; import de.unioninvestment.eai.portal.support.vaadin.container.GenericItem; import de.unioninvestment.eai.portal.support.vaadin.container.MetaData; import de.unioninvestment.eai.portal.support.vaadin.container.UpdateContext; public class ReSTDelegateImpl implements ReSTDelegate { static final class ConstantStringProvider extends Closure<Object> { private final String string; private static final long serialVersionUID = 1L; ConstantStringProvider(Object owner, String newQueryUrl) { super(owner); this.string = newQueryUrl; } public String doCall() { return string; } } private static final List<Object[]> EMPTY_RESULT = unmodifiableList(new LinkedList<Object[]>()); private static final Logger LOGGER = LoggerFactory .getLogger(ReSTDelegateImpl.class); private ReSTContainerConfig config; private ScriptBuilder scriptBuilder; private MetaData metaData; HttpClient httpClient = new DefaultHttpClient(); PayloadParser parser; PayloadCreator creator; private ReSTContainer container; private Closure<Object> baseUrlProvider; private Closure<Object> queryUrlProvider; private AuditLogger auditLogger; public ReSTDelegateImpl(ReSTContainerConfig containerConfig, ReSTContainer container, Realm realm, ScriptBuilder scriptBuilder, AuditLogger auditLogger) { this.config = containerConfig; this.container = container; this.scriptBuilder = scriptBuilder; this.auditLogger = auditLogger; this.metaData = extractMetaData(); this.parser = createParser(); this.creator = createCreator(); this.baseUrlProvider = scriptBuilder.buildClosure(config.getBaseUrl()); this.queryUrlProvider = scriptBuilder.buildClosure(config.getQuery() .getUrl()); if (realm != null && httpClient instanceof DefaultHttpClient) { // for Testing => DefaultHttpClient cannot be fully mocked due to // final methods realm.applyBasicAuthentication((DefaultHttpClient) httpClient); } } private PayloadCreator createCreator() { if (config.getFormat() == ReSTFormatConfig.JSON) { return new JsonCreator(container, scriptBuilder); } else if (config.getFormat() == ReSTFormatConfig.XML) { return new XmlCreator(container, scriptBuilder); } else { throw new TechnicalCrudPortletException("Unknown ReST format: " + config.getFormat()); } } private PayloadParser createParser() { if (config.getFormat() == ReSTFormatConfig.JSON) { return new JsonParser(config, scriptBuilder); } else if (config.getFormat() == ReSTFormatConfig.XML) { return new XmlParser(config, scriptBuilder); } else { throw new TechnicalCrudPortletException("Unknown ReST format: " + config.getFormat()); } } private MetaData extractMetaData() { List<ReSTAttributeConfig> attributes = config.getQuery().getAttribute(); List<Column> columns = new ArrayList<Column>(attributes.size()); for (ReSTAttributeConfig attribute : attributes) { columns.add(new Column(attribute.getName(), attribute.getType(), attribute.isReadonly(), attribute.isRequired(), attribute .isPrimaryKey(), null)); } boolean insertSupported = config.getInsert() != null; boolean updateSupported = config.getUpdate() != null; boolean deleteSupported = config.getDelete() != null; return new MetaData(columns, insertSupported, updateSupported, deleteSupported, false, false); } @Override public MetaData getMetaData() { return metaData; } @Override public List<Object[]> getRows() { String queryUrl = (String) queryUrlProvider.call(); if (StringUtils.isBlank(queryUrl)) { return EMPTY_RESULT; } HttpGet request = createQueryRequest(); try { HttpResponse response = httpClient.execute(request); expectAnyStatusCode(response, HttpStatus.SC_OK); return parser.getRows(response); } catch (ClientProtocolException e) { LOGGER.error("Error during restful query", e); throw new BusinessException("portlet.crud.error.rest.io", e.getMessage()); } catch (IOException e) { LOGGER.error("Error during restful query", e); throw new BusinessException("portlet.crud.error.rest.io", e.getMessage()); } finally { request.releaseConnection(); } } private HttpGet createQueryRequest() { String queryUrl = queryUrlProvider.call().toString(); URI uri = createURI(queryUrl); HttpGet request = new HttpGet(uri); String mimetype = creator.getMimeType(); if (config.getMimetype() != null) { mimetype = config.getMimetype(); } request.addHeader("Accept", mimetype); return request; } private URI createURI(String postfix) { String uri = postfix; if (baseUrlProvider != null) { Object baseUrl = baseUrlProvider.call(); if (baseUrl != null) { uri = baseUrl.toString() + postfix; } } try { return URI.create(uri); } catch (IllegalArgumentException e) { throw new InvalidConfigurationException( "portlet.crud.error.config.rest.invalidUrl", uri); } } @Override public void setFilters(Filter[] filters) { throw new UnsupportedOperationException( "Server side filtering is not supported by the ReST backend"); } @Override public void update(List<GenericItem> items, UpdateContext context) { context.requireRefresh(); try { for (GenericItem item : items) { if (item.isNewItem() || item.isModified()) { ReSTChangeConfig changeConfig = findChangeConfig(item); ReSTChangeMethodConfig method = findRequestMethod(item); if (method == ReSTChangeMethodConfig.POST) { sendPostRequest(item, changeConfig); } else { sendPutRequest(item, changeConfig); } } else if (item.isDeleted()) { sendDeleteRequest(item); } } } catch (ClientProtocolException e) { LOGGER.error("Error during restful operation", e); throw new BusinessException("portlet.crud.error.rest.io", e.getMessage()); } catch (IOException e) { LOGGER.error("Error during restful operation", e); throw new BusinessException("portlet.crud.error.rest.io", e.getMessage()); } } private ReSTChangeConfig findChangeConfig(GenericItem item) { ReSTChangeConfig changeConfig = item.isNewItem() ? config.getInsert() : config.getUpdate(); return changeConfig; } private ReSTChangeMethodConfig findRequestMethod(GenericItem item) { ReSTChangeMethodConfig method = null; if (item.isNewItem()) { method = config.getInsert().getMethod(); if (method == null) { method = ReSTChangeMethodConfig.POST; } } else { method = config.getUpdate().getMethod(); if (method == null) { method = ReSTChangeMethodConfig.PUT; } } return method; } private void sendPostRequest(GenericItem item, ReSTChangeConfig changeConfig) throws IOException, ClientProtocolException { byte[] content = creator.create(item, changeConfig.getValue(), config.getCharset()); URI uri = createURI(item, changeConfig.getUrl()); HttpPost request = new HttpPost(uri); ContentType contentType = createContentType(); request.setEntity(new ByteArrayEntity(content, contentType)); try { HttpResponse response = httpClient.execute(request); auditLogger.auditReSTRequest(request.getMethod(), uri.toString(), new String(content, config.getCharset()), response .getStatusLine().toString()); expectAnyStatusCode(response, HttpStatus.SC_CREATED, HttpStatus.SC_NO_CONTENT); } finally { request.releaseConnection(); } } private void sendPutRequest(GenericItem item, ReSTChangeConfig changeConfig) throws ClientProtocolException, IOException { byte[] content = creator.create(item, changeConfig.getValue(), config.getCharset()); URI uri = createURI(item, changeConfig.getUrl()); HttpPut request = new HttpPut(uri); ContentType contentType = createContentType(); request.setEntity(new ByteArrayEntity(content, contentType)); try { HttpResponse response = httpClient.execute(request); auditLogger.auditReSTRequest(request.getMethod(), uri.toString(), new String(content, config.getCharset()), response .getStatusLine().toString()); expectAnyStatusCode(response, HttpStatus.SC_OK, HttpStatus.SC_NO_CONTENT); } finally { request.releaseConnection(); } } private void expectAnyStatusCode(HttpResponse response, int... acceptedStatusCodes) { StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); for (int i = 0; i < acceptedStatusCodes.length; i++) { if (acceptedStatusCodes[i] == statusCode) { return; } } throw new BusinessException("portlet.crud.error.rest.wrongStatus", statusCode, statusLine.getReasonPhrase()); } private ContentType createContentType() { String mimetype = creator.getMimeType(); if (config.getMimetype() != null) { mimetype = config.getMimetype(); } return ContentType.create(mimetype, config.getCharset()); } private void sendDeleteRequest(GenericItem item) throws ClientProtocolException, IOException { URI uri = createURI(item, config.getDelete().getUrl()); HttpDelete request = new HttpDelete(uri); try { HttpResponse response = httpClient.execute(request); auditLogger.auditReSTRequest(request.getMethod(), uri.toString(), response.getStatusLine().toString()); expectAnyStatusCode(response, HttpStatus.SC_OK, HttpStatus.SC_ACCEPTED, // marked for deletion HttpStatus.SC_NO_CONTENT); } finally { request.releaseConnection(); } } private URI createURI(GenericItem item, GroovyScript uriScript) { GenericContainerRowId containerRowId = new GenericContainerRowId( item.getId(), container.getPrimaryKeyColumns()); GenericContainerRow containerRow = new GenericContainerRow( containerRowId, item, container, false, true); ScriptRow row = new ScriptRow(containerRow); Object postfix = scriptBuilder.buildClosure(uriScript).call(row); return createURI(postfix.toString()); } public void setBaseUrl(final String newBaseUrl) { this.baseUrlProvider = new ConstantStringProvider(null, newBaseUrl); } public void setQueryUrl(final String newQueryUrl) { this.queryUrlProvider = new ConstantStringProvider(null, newQueryUrl); } }