Rewrite internal eureka based links to external links in zuul proxy - spring-boot

I am writing a microservice based application with spring-boot services.
For communication I use REST (with hateoas links). Each service registers with eureka, so I the links I provide are based on these names, so that the ribbon enhanced resttemplates can use the loadbalancing and failover capabilities of the stack.
This works fine for internal communication, but I have a single page admin app that accesses the services through a zuul based reverse proxy.
When the links are using the real hostname and port the links are correctly rewritten to match the url visible from the outside. This of course doesn't work for the symbolic links that I need in the inside...
So internally I have links like:
http://adminusers/myfunnyusername
The zuul proxy should rewrite this to
http://localhost:8090/api/adminusers/myfunnyusername
Is there something that I am missing in zuul or somewhere along the way that would make this easier?
Right now I'm thinking how to reliably rewrite the urls myself without collateral damage.
There should be a simpler way, right?

Aparrently Zuul is not capable of rewriting links from the symbolic eureka names to "outside links".
For that I just wrote a Zuul filter that parses the json response, and looks for "links" nodes and rewrites the links to my schema.
For example, my services are named: adminusers and restaurants
The result from the service has links like http://adminusers/{id} and http://restaurants/cuisine/{id}
Then it would be rewritten to
http://localhost:8090/api/adminusers/{id} and http://localhost:8090/api/restaurants/cuisine/{id}
private String fixLink(String href) {
//Right now all "real" links contain ports and loadbalanced links not
//TODO: precompile regexes
if (!href.matches("http[s]{0,1}://[a-zA-Z0-9]+:[0-9]+.*")) {
String newRef = href.replaceAll("http[s]{0,1}://([a-zA-Z0-9]+)", BasicLinkBuilder.linkToCurrentMapping().toString() + "/api/$1");
LOG.info("OLD: {}", href);
LOG.info("NEW: {}", newRef);
href = newRef;
}
return href;
}
(This needs to be optimized a little, as you could compile the regexp only once, I'll do that once I'm sure that this is what I really need in the long run)
UPDATE
Thomas asked for the full filter code, so here it is. Be aware, it makes some assumptions about the URLs! I assume that internal links do not contain a port and have the servicename as host, which is a valid assumption for eureka based apps, as ribbon etc. are able to work with those. I rewrite that to a link like $PROXY/api/$SERVICENAME/...
Feel free to use this code.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.CharStreams;
import com.netflix.util.Pair;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.hateoas.mvc.BasicLinkBuilder;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkNotNull;
#Component
public final class ContentUrlRewritingFilter extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(ContentUrlRewritingFilter.class);
private static final String CONTENT_TYPE = "Content-Type";
private static final ImmutableSet<MediaType> DEFAULT_SUPPORTED_TYPES = ImmutableSet.of(MediaType.APPLICATION_JSON);
private final String replacement;
private final ImmutableSet<MediaType> supportedTypes;
//Right now all "real" links contain ports and loadbalanced links not
private final Pattern detectPattern = Pattern.compile("http[s]{0,1}://[a-zA-Z0-9]+:[0-9]+.*");
private final Pattern replacePattern;
public ContentUrlRewritingFilter() {
this.replacement = checkNotNull("/api/$1");
this.supportedTypes = ImmutableSet.copyOf(checkNotNull(DEFAULT_SUPPORTED_TYPES));
replacePattern = Pattern.compile("http[s]{0,1}://([a-zA-Z0-9]+)");
}
private static boolean containsContent(final RequestContext context) {
assert context != null;
return context.getResponseDataStream() != null || context.getResponseBody() != null;
}
private static boolean supportsType(final RequestContext context, final Collection<MediaType> supportedTypes) {
assert supportedTypes != null;
for (MediaType supportedType : supportedTypes) {
if (supportedType.isCompatibleWith(getResponseMediaType(context))) return true;
}
return false;
}
private static MediaType getResponseMediaType(final RequestContext context) {
assert context != null;
for (final Pair<String, String> header : context.getZuulResponseHeaders()) {
if (header.first().equalsIgnoreCase(CONTENT_TYPE)) {
return MediaType.parseMediaType(header.second());
}
}
return MediaType.APPLICATION_OCTET_STREAM;
}
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
return 100;
}
#Override
public boolean shouldFilter() {
final RequestContext context = RequestContext.getCurrentContext();
return hasSupportedBody(context);
}
public boolean hasSupportedBody(RequestContext context) {
return containsContent(context) && supportsType(context, this.supportedTypes);
}
#Override
public Object run() {
try {
rewriteContent(RequestContext.getCurrentContext());
} catch (final Exception e) {
Throwables.propagate(e);
}
return null;
}
private void rewriteContent(final RequestContext context) throws Exception {
assert context != null;
String responseBody = getResponseBody(context);
if (responseBody != null) {
ObjectMapper mapper = new ObjectMapper();
LinkedHashMap<String, Object> map = mapper.readValue(responseBody, LinkedHashMap.class);
traverse(map);
String body = mapper.writeValueAsString(map);
context.setResponseBody(body);
}
}
private String getResponseBody(RequestContext context) throws IOException {
String responseData = null;
if (context.getResponseBody() != null) {
context.getResponse().setCharacterEncoding("UTF-8");
responseData = context.getResponseBody();
} else if (context.getResponseDataStream() != null) {
context.getResponse().setCharacterEncoding("UTF-8");
try (final InputStream responseDataStream = context.getResponseDataStream()) {
//FIXME What about character encoding of the stream (depends on the response content type)?
responseData = CharStreams.toString(new InputStreamReader(responseDataStream));
}
}
return responseData;
}
private void traverse(Map<String, Object> node) {
for (Map.Entry<String, Object> entry : node.entrySet()) {
if (entry.getKey().equalsIgnoreCase("links") && entry.getValue() instanceof Collection) {
replaceLinks((Collection<Map<String, String>>) entry.getValue());
} else {
if (entry.getValue() instanceof Collection) {
traverse((Collection) entry.getValue());
} else if (entry.getValue() instanceof Map) {
traverse((Map<String, Object>) entry.getValue());
}
}
}
}
private void traverse(Collection<Map> value) {
for (Object entry : value) {
if (entry instanceof Collection) {
traverse((Collection) entry);
} else if (entry instanceof Map) {
traverse((Map<String, Object>) entry);
}
}
}
private void replaceLinks(Collection<Map<String, String>> value) {
for (Map<String, String> node : value) {
if (node.containsKey("href")) {
node.put("href", fixLink(node.get("href")));
} else {
LOG.debug("Link Node did not contain href! {}", value.toString());
}
}
}
private String fixLink(String href) {
if (!detectPattern.matcher(href).matches()) {
href = replacePattern.matcher(href).replaceAll(BasicLinkBuilder.linkToCurrentMapping().toString() + replacement);
}
return href;
}
}
Improvements are welcome :-)

Have a look at HATEOAS paths are invalid when using an API Gateway in a Spring Boot app
If properly configured, ZUUL should add the "X-Forwarded-Host" header to all the forwarded requests, which Spring-hateoas respects and modifies the links appropriately.

Related

AutoCompleteTextView doesn't show full address (new Places SDK)

I migrated from the old Places SDK to the new Places SDK (including writing a new adapter), and now when typing an address into my AutoCompleteTextView it shows only the Place Names in the drop-down list (i.e. addresses but without city, state, country), but I need it to show the full address.
Here is my adapter:
import android.content.Context;
import android.graphics.Typeface;
import android.text.style.CharacterStyle;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.libraries.places.api.model.AutocompletePrediction;
import com.google.android.libraries.places.api.model.AutocompleteSessionToken;
import com.google.android.libraries.places.api.model.RectangularBounds;
import com.google.android.libraries.places.api.model.TypeFilter;
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest;
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsResponse;
import com.google.android.libraries.places.api.net.PlacesClient;
import java.util.List;
public class PlaceAutocompleteAdapterNew extends ArrayAdapter<AutocompletePrediction> implements Filterable
{
PlacesClient placesClient;
AutocompleteSessionToken token;
private static final CharacterStyle STYLE_BOLD = new StyleSpan(Typeface.BOLD);
private List<AutocompletePrediction> mResultList;
private List<AutocompletePrediction> tempResult;
Context context;
private String TAG="PlaceAutoCompleteAdapter";
public PlaceAutocompleteAdapterNew(Context context,PlacesClient placesClient,AutocompleteSessionToken token) {
super(context,android.R.layout.simple_expandable_list_item_1,android.R.id.text1);
this.context=context;
this.placesClient=placesClient;
this.token=token;
}
public View getView(int position, View convertView, ViewGroup parent) {
View row = super.getView(position, convertView, parent);
AutocompletePrediction item = getItem(position);
TextView textView1 = (TextView) row.findViewById(android.R.id.text1);
textView1.setText(item.getPrimaryText(STYLE_BOLD));
return row;
}
#Override
public int getCount() {
return mResultList.size();
}
#Override
public AutocompletePrediction getItem(int position) {
return mResultList.get(position);
}
#Override
public Filter getFilter() {
return new Filter() {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
// Skip the autocomplete query if no constraints are given.
if (constraint != null) {
// Query the autocomplete API for the (constraint) search string.
mResultList = getAutoComplete(constraint);
if (mResultList != null) {
// The API successfully returned results.
results.values = mResultList;
results.count = mResultList.size();
}
}
return results;
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
// The API returned at least one result, update the data.
notifyDataSetChanged();
} else {
// The API did not return any results, invalidate the data set.
notifyDataSetInvalidated();
}
}
#Override
public CharSequence convertResultToString(Object resultValue) {
// Override this method to display a readable result in the AutocompleteTextView
// when clicked.
if (resultValue instanceof AutocompletePrediction) {
return ((AutocompletePrediction) resultValue).getFullText(null);
} else {
return super.convertResultToString(resultValue);
}
}
};
}
private List<AutocompletePrediction> getAutoComplete(CharSequence constraint){
// Create a new token for the autocomplete session. Pass this to FindAutocompletePredictionsRequest,
// and once again when the user makes a selection (for example when calling fetchPlace()).
AutocompleteSessionToken token = AutocompleteSessionToken.newInstance();
// Create a RectangularBounds object.
// Use the builder to create a FindAutocompletePredictionsRequest.
FindAutocompletePredictionsRequest request = FindAutocompletePredictionsRequest.builder()
// Call either setLocationBias() OR setLocationRestriction().
//.setLocationBias(bounds)
//.setLocationRestriction(bounds)
.setTypeFilter(TypeFilter.ADDRESS)
.setSessionToken(token)
.setQuery(constraint.toString())
.build();
placesClient.findAutocompletePredictions(request).addOnSuccessListener(new OnSuccessListener<FindAutocompletePredictionsResponse>() {
#Override
public void onSuccess(FindAutocompletePredictionsResponse response) {
for (AutocompletePrediction prediction : response.getAutocompletePredictions()) {
Log.i(TAG, prediction.getPrimaryText(null).toString());
}
tempResult=response.getAutocompletePredictions();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
if (exception instanceof ApiException) {
ApiException apiException = (ApiException) exception;
Log.e(TAG, "Place not found: " + apiException.getStatusCode());
}
}
});
return tempResult;
}
}
How can I show the full addresses in the drop-down list?
getAutoComplete is returning tempResult. This is a List that contains fullText (the full address), primaryText (just the address without city, state, country), and other items. So the fullText is what I want, which is being returned, but the primaryText is what is being displayed in the AutoCompleteTextView. How can I fix this?
I changed this line:
textView1.setText(item.getPrimaryText(STYLE_BOLD));
to this:
textView1.setText(item.getFullText(STYLE_BOLD));

How to set the default JDBC URL value in H2 console using spring boot

I have enable the H2 console in spring boot. However, when I open the console connection page the default url is the one staved in the H2 console history. How can i configure the project to populate the URL to be the same as spring.datasource.url on project start? Currently I set the url in the console manually but I would like to have it setup automatically by the project itself.
yaml:
spring:
h2:
console:
enabled: true
path: /admin/h2
datasource:
url: jdbc:h2:mem:foobar
update:
I know that the last connection settings are saved to ~/.h2.server.properties but what I need is to set the properties from the starting application potentially, potentially switching between several of them
There is no hook provided to fill in settings.
The good news is that we can change that with a bit of code.
Current state
The Login screen is created in WebApp.index()
String[] settingNames = server.getSettingNames();
String setting = attributes.getProperty("setting");
if (setting == null && settingNames.length > 0) {
setting = settingNames[0];
}
String combobox = getComboBox(settingNames, setting);
session.put("settingsList", combobox);
ConnectionInfo info = server.getSetting(setting);
if (info == null) {
info = new ConnectionInfo();
}
session.put("setting", PageParser.escapeHtmlData(setting));
session.put("name", PageParser.escapeHtmlData(setting));
session.put("driver", PageParser.escapeHtmlData(info.driver));
session.put("url", PageParser.escapeHtmlData(info.url));
session.put("user", PageParser.escapeHtmlData(info.user));
return "index.jsp";
We want to tap into server.getSettingNames(), and precisely into server.getSettings() used underneath.
synchronized ArrayList<ConnectionInfo> getSettings() {
ArrayList<ConnectionInfo> settings = new ArrayList<>();
if (connInfoMap.size() == 0) {
Properties prop = loadProperties();
if (prop.size() == 0) {
for (String gen : GENERIC) {
ConnectionInfo info = new ConnectionInfo(gen);
settings.add(info);
updateSetting(info);
}
} else {
for (int i = 0;; i++) {
String data = prop.getProperty(Integer.toString(i));
if (data == null) {
break;
}
ConnectionInfo info = new ConnectionInfo(data);
settings.add(info);
updateSetting(info);
}
}
} else {
settings.addAll(connInfoMap.values());
}
Collections.sort(settings);
return settings;
}
The plan
disable ServletRegistrationBean<WebServlet> created by H2ConsoleAutoConfiguration
replace it with our config class with a subclass of WebServlet
our CustomH2WebServlet will override init and register CustomH2WebServer (subclass of WebServer)
in CustomH2WebServer we override getSettings() and we are done
The Code
#EnableConfigurationProperties({H2ConsoleProperties.class, DataSourceProperties.class})
#Configuration
public class H2Config {
private final H2ConsoleProperties h2ConsoleProperties;
private final DataSourceProperties dataSourceProperties;
public H2Config(H2ConsoleProperties h2ConsoleProperties, DataSourceProperties dataSourceProperties) {
this.h2ConsoleProperties = h2ConsoleProperties;
this.dataSourceProperties = dataSourceProperties;
}
#Bean
public ServletRegistrationBean<WebServlet> h2Console() {
String path = this.h2ConsoleProperties.getPath();
String urlMapping = path + (path.endsWith("/") ? "*" : "/*");
ServletRegistrationBean<WebServlet> registration = new ServletRegistrationBean<>(
new CustomH2WebServlet(this.dataSourceProperties.getUrl()), urlMapping);
H2ConsoleProperties.Settings settings = this.h2ConsoleProperties.getSettings();
if (settings.isTrace()) {
registration.addInitParameter("trace", "");
}
if (settings.isWebAllowOthers()) {
registration.addInitParameter("webAllowOthers", "");
}
return registration;
}
}
package org.h2.server.web;
import javax.servlet.ServletConfig;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Enumeration;
public class CustomH2WebServlet extends WebServlet {
private final String dbUrl;
public CustomH2WebServlet(String dbUrl) {
this.dbUrl = dbUrl;
}
#Override
public void init() {
ServletConfig config = getServletConfig();
Enumeration<?> en = config.getInitParameterNames();
ArrayList<String> list = new ArrayList<>();
while (en.hasMoreElements()) {
String name = en.nextElement().toString();
String value = config.getInitParameter(name);
if (!name.startsWith("-")) {
name = "-" + name;
}
list.add(name);
if (value.length() > 0) {
list.add(value);
}
}
String[] args = list.toArray(new String[0]);
WebServer server = new CustomH2WebServer(dbUrl);
server.setAllowChunked(false);
server.init(args);
setServerWithReflection(this, server);
}
private static void setServerWithReflection(final WebServlet classInstance, final WebServer newValue) {
try {
final Field field = WebServlet.class.getDeclaredField("server");
field.setAccessible(true);
field.set(classInstance, newValue);
}
catch (SecurityException|NoSuchFieldException|IllegalArgumentException|IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
}
package org.h2.server.web;
import java.util.ArrayList;
import java.util.Collections;
class CustomH2WebServer extends WebServer {
private final String connectionInfo;
CustomH2WebServer(String dbUrl) {
this.connectionInfo = "Test H2 (Embedded)|org.h2.Driver|" +dbUrl+"|sa";
}
synchronized ArrayList<ConnectionInfo> getSettings() {
ArrayList<ConnectionInfo> settings = new ArrayList<>();
ConnectionInfo info = new ConnectionInfo(connectionInfo);
settings.add(info);
updateSetting(info);
Collections.sort(settings);
return settings;
}
}
spring.h2.console.enabled=false
spring.datasource.url=jdbc:h2:mem:foobar
Everything went smoolthly, except of one private field that needed to be set via reflection.
The code provided works with H2 1.4.199
Inspired in #Lesiak's answer, using a simple and easier configuration class
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import lombok.extern.apachecommons.CommonsLog;
import org.h2.server.web.ConnectionInfo;
import org.h2.server.web.WebServer;
import org.h2.server.web.WebServlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#CommonsLog
#Configuration
#ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true", matchIfMissing = false)
public class H2ConsoleConfiguration {
#Bean
ServletRegistrationBean<WebServlet> h2ConsoleRegistrationBean(final ServletRegistrationBean<WebServlet> h2Console, final DataSourceProperties dataSourceProperties) {
h2Console.setServlet(new WebServlet() {
#Override
public void init() {
super.init();
updateWebServlet(this, dataSourceProperties);
}
});
return h2Console;
}
public static void updateWebServlet(final WebServlet webServlet, DataSourceProperties dataSourceProperties) {
try {
updateWebServer(getWebServer(webServlet), dataSourceProperties);
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InvocationTargetException | NullPointerException ex) {
log.error("Unable to set a custom ConnectionInfo for H2 console", ex);
}
}
public static WebServer getWebServer(final WebServlet webServlet) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
final Field field = WebServlet.class.getDeclaredField("server");
field.setAccessible(true);
return (WebServer) field.get(webServlet);
}
public static void updateWebServer(final WebServer webServer, final DataSourceProperties dataSourceProperties) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
final ConnectionInfo connectionInfo = new ConnectionInfo(String.format("Generic Spring Datasource|%s|%s|%s", dataSourceProperties.determineDriverClassName(), dataSourceProperties.determineUrl(), dataSourceProperties.determineUsername()));
final Method method = WebServer.class.getDeclaredMethod("updateSetting", ConnectionInfo.class);
method.setAccessible(true);
method.invoke(webServer, connectionInfo);
}
}

NiFI "unable to find flowfile content"

I am using nifi 1.6 and get the following errors when trying to modify a clone of an incoming flowFile:
[1]"unable to find content for FlowFile: ... MissingFlowFileException
...
Caused by ContentNotFoundException: Could not find contetn for StandardClaim
...
Caused by java.io.EOFException: null"
[2]"FlowFileHandlingException: StandardFlowFileRecord... is not known in this session"
The first error occurs when trying to access the contents of the flow file, the second when removing the flow file from the session (within a catch of the first). This process is known to have worked under nifi 0.7.
The basic process is:
Clone the incoming flow file
Write to the clone
Write to the clone again (some additional formatting)
Repeat 1-3
The error occurs on the second iteration step 3.
An interesting point is that if immediately after the clone is performed, a session.read of the clone is done everything works fine. The read seems to reset some pointer.
I have created unit tests for this processor, but they do not fail in either case.
Below is code simplified from the actual version in use that demonstrates the issue. (The development system is not connected so I had to copy the code. Please forgive any typos - it should be close. This is also why a full stack trace is not provided.) The processor doing the work has a property to determine if an immediate read should be done, or not. So both scenarios can be performed easily. To set it up, all that is needed is a GetFile processor to supply the input and terminators for the output from the SampleCloningProcessor. A sample input file is included as well. The meat of the code is in the onTrigger and manipulate methods. The manipulation in this simplified version really don't do anything but copy the input to the output.
Any insights into why this is happening and suggestions for corrections will be appreciated - thanks.
SampleCloningProcessor.java
processor sample.package.cloning
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.util.Arrays;
import java.util.Hashset;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.nifi.annotation.documentaion.CapabilityDescription;
import org.apache.nifi.annotation.documentaion.Tags;
import org.apache.nifi.componets.PropertyDescriptor;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.AbstractProcessor;
import org.apache.nifi.processor.ProcessorContext;
import org.apache.nifi.processor.ProcessorSession;
import org.apache.nifi.processor.ProcessorInitioalizationContext;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.io.InputStreamCalback;
import org.apache.nifi.processor.io.OutputStreamCalback;
import org.apache.nifi.processor.io.StreamCalback;
import org.apache.nifi.processor.util.StandardValidators;
import com.google.gson.Gson;
#Tags({"example", "clone"})
#CapabilityDescription("Demonsrates cloning of flowfile failure.")
public class SampleCloningProcessor extend AbstractProcessor {
/* Determines if an immediate read is performed after cloning of inoming flowfile. */
public static final PropertyDescriptor IMMEDIATE_READ = new PropertyDescriptor.Builder()
.name("immediateRead")
.description("Determines if processor runs successfully. If a read is done immediatly "
+ "after the clone of the incoming flowFile, then the processor should run successfully.")
.required(true)
.allowableValues("true", "false")
.defaultValue("true")
.addValidator(StandardValidators.BOLLEAN_VALIDATOR)
.build();
public static final Relationship SUCCESS = new Relationship.Builder().name("success").
description("No unexpected errors.").build();
public static final Relationship FAILURE = new Relationship.Builder().name("failure").
description("Errors were thrown.").build();
private Set<Relationship> relationships;
private List<PropertyDescriptors> properties;
#Override
public void init(final ProcessorInitializationContext contex) {
relationships = new HashSet<>(Arrays.asList(SUCCESS, FAILURE));
properties = new Arrays.asList(IMMEDIATE_READ);
}
#Override
public Set<Relationship> getRelationships() {
return this.relationships;
}
#Override
public List<PropertyDescriptor> getSuppprtedPropertyDescriptors() {
return this.properties;
}
#Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
FlowFile incomingFlowFile = session.get();
if (incomingFlowFile == null) {
return;
}
try {
final InfileReader inFileReader = new InfileReader();
session.read(incomingFlowFile, inFileReader);
Product product = infileReader.getProduct();
boolean transfer = false;
getLogger().info("\tSession :\n" + session);
getLogger().info("\toriginal :\n" + incomingFlowFile);
for(int i = 0; i < 2; i++) {
transfer = manipulate(context, session, inclmingFlowFile, product);
}
} catch (Exception e) {
getLogger().error(e.getMessage(), e);
session.rollback(true);
}
}
private boolean manipuate(final ProcessContext context, final ProcessSession session
final FlowFile incomingFlowFile, final Product product) {
boolean transfer = false;
FlowFile outgoingFlowFile = null;
boolean immediateRead = context.getProperty(IMMEDIATE_READ).asBoolean();
try {
//Clone incoming flowFile
outgoinFlowFile = session.clone(incomingFlowFile);
getLogger().info("\tclone outgoing :\n" + outgoingFlowFile);
if(immediateRead) {
readFlowFile(session, outgoingFlowFile);
}
//First write into clone
StageOneWrite stage1Write = new StaeOneWrite(product);
outgoingFlowFile = session.write(outgoingFlowFile, stage1Write);
getLogger().info("\twrite outgoing :\n" + outgoingFlowFile);
// Format the cloned file with another write
outgoingFlowFile = formatFlowFile(outgoingFlowFile, session)
getLogger().info("\format outgoing :\n" + outgoingFlowFile);
session.transfer(outgoingFlowFile, SUCCESS);
transfer != true;
} catch(Exception e)
getLogger().error(e.getMessage(), e);
if(outgoingFlowFile ! = null) {
session.remove(outgoingFlowFile);
}
}
return transfer;
}
private void readFlowFile(fainl ProcessSession session, fianl Flowfile flowFile) {
session.read(flowFile, new InputStreamCallback() {
#Override
public void process(Final InputStream in) throws IOException {
try (Scanner scanner = new Scanner(in)) {
scanner.useDelimiter("\\A").next();
}
}
});
}
private FlowFile formatFlowFile(fainl ProcessSession session, FlowFile flowfile) {
OutputFormatWrite formatWrite = new OutputFormatWriter();
flowfile = session.write(flowFile, formatWriter);
return flowFile;
}
private static class OutputFormatWriter implement StreamCallback {
#Override
public void process(final InputStream in, final OutputStream out) throws IOException {
try {
IOUtils.copy(in. out);
out.flush();
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(out);
}
}
}
private static class StageOneWriter implements OutputStreamCallback {
private Product product = null;
public StageOneWriter(Produt product) {
this.product = product;
}
#Override
public void process(final OutputStream out) throws IOException {
final Gson gson = new Gson();
final String json = gson.toJson(product);
out.write(json.getBytes());
}
}
private static class InfileReader implements InputStreamCallback {
private Product product = null;
public StageOneWriter(Produt product) {
this.product = product;
}
#Override
public void process(final InputStream out) throws IOException {
product = null;
final Gson gson = new Gson();
Reader inReader = new InputStreamReader(in, "UTF-8");
product = gson.fromJson(inreader, Product.calss);
}
public Product getProduct() {
return product;
}
}
SampleCloningProcessorTest.java
package sample.processors.cloning;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
import org.junit.Before;
import org.junit.Test;
public class SampleCloningProcessorTest {
final satatic String flowFileContent = "{"
+ "\"cost\": \"cost 1\","
+ "\"description\": \"description","
+ "\"markup\": 1.2"
+ "\"name\":\"name 1\","
+ "\"supplier\":\"supplier 1\","
+ "}";
private TestRunner testRunner;
#Before
public void init() {
testRunner = TestRunner.newTestRunner(SampleCloningProcessor.class);
testRunner.enqueue(flowFileContent);
}
#Test
public void testProcessorImmediateRead() {
testRunner.setProperty(SampleCloningProcessor.IMMEDIATE_READ, "true");
testRunner.run();
testRinner.assertTransferCount("success", 2);
}
#Test
public void testProcessorImmediateRead_false() {
testRunner.setProperty(SampleCloningProcessor.IMMEDIATE_READ, "false");
testRunner.run();
testRinner.assertTransferCount("success", 2);
}
}
Product.java
package sample.processors.cloning;
public class Product {
private String name;
private String description;
private String supplier;
private String cost;
private float markup;
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescriptione(final String description) {
this.description = description;
}
public String getSupplier() {
return supplier;
}
public void setSupplier(final String supplier) {
this.supplier = supplier;
}
public String getCost() {
return cost;
}
public void setCost(final String cost) {
this.cost = cost;
}
public float getMarkup() {
return markup;
}
public void setMarkup(final float name) {
this.markup = markup;
}
}
product.json A sample input file.
{
"const" : "cost 1",
"description" : "description 1",
"markup" : 1.2,
"name" : "name 1",
"supplier" : "supplier 1"
}
Reported as a bug in Nifi. Being addressed by https://issues.apache.org/jira/browse/NIFI-5879

Apache CXF Interceptors: Unable to modify the response Stream in a Out Interceptor [duplicate]

I would like to modify an outgoing SOAP Request.
I would like to remove 2 xml nodes from the Envelope's body.
I managed to set up an Interceptor and get the generated String value of the message set to the endpoint.
However, the following code does not seem to work as the outgoing message is not edited as expected. Does anyone have some code or ideas on how to do this?
public class MyOutInterceptor extends AbstractSoapInterceptor {
public MyOutInterceptor() {
super(Phase.SEND);
}
public void handleMessage(SoapMessage message) throws Fault {
// Get message content for dirty editing...
StringWriter writer = new StringWriter();
CachedOutputStream cos = (CachedOutputStream)message.getContent(OutputStream.class);
InputStream inputStream = cos.getInputStream();
IOUtils.copy(inputStream, writer, "UTF-8");
String content = writer.toString();
// remove the substrings from envelope...
content = content.replace("<idJustification>0</idJustification>", "");
content = content.replace("<indicRdv>false</indicRdv>", "");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(content.getBytes(Charset.forName("UTF-8")));
message.setContent(OutputStream.class, outputStream);
}
Based on the first comment, I created an abstract class which can easily be used to change the whole soap envelope.
Just in case someone wants a ready-to-use code part.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor;
import org.apache.cxf.io.CachedOutputStream;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.log4j.Logger;
/**
* http://www.mastertheboss.com/jboss-web-services/apache-cxf-interceptors
* http://stackoverflow.com/questions/6915428/how-to-modify-the-raw-xml-message-of-an-outbound-cxf-request
*
*/
public abstract class MessageChangeInterceptor extends AbstractPhaseInterceptor<Message> {
public MessageChangeInterceptor() {
super(Phase.PRE_STREAM);
addBefore(SoapPreProtocolOutInterceptor.class.getName());
}
protected abstract Logger getLogger();
protected abstract String changeOutboundMessage(String currentEnvelope);
protected abstract String changeInboundMessage(String currentEnvelope);
public void handleMessage(Message message) {
boolean isOutbound = false;
isOutbound = message == message.getExchange().getOutMessage()
|| message == message.getExchange().getOutFaultMessage();
if (isOutbound) {
OutputStream os = message.getContent(OutputStream.class);
CachedStream cs = new CachedStream();
message.setContent(OutputStream.class, cs);
message.getInterceptorChain().doIntercept(message);
try {
cs.flush();
IOUtils.closeQuietly(cs);
CachedOutputStream csnew = (CachedOutputStream) message.getContent(OutputStream.class);
String currentEnvelopeMessage = IOUtils.toString(csnew.getInputStream(), "UTF-8");
csnew.flush();
IOUtils.closeQuietly(csnew);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Outbound message: " + currentEnvelopeMessage);
}
String res = changeOutboundMessage(currentEnvelopeMessage);
if (res != null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Outbound message has been changed: " + res);
}
}
res = res != null ? res : currentEnvelopeMessage;
InputStream replaceInStream = IOUtils.toInputStream(res, "UTF-8");
IOUtils.copy(replaceInStream, os);
replaceInStream.close();
IOUtils.closeQuietly(replaceInStream);
os.flush();
message.setContent(OutputStream.class, os);
IOUtils.closeQuietly(os);
} catch (IOException ioe) {
getLogger().warn("Unable to perform change.", ioe);
throw new RuntimeException(ioe);
}
} else {
try {
InputStream is = message.getContent(InputStream.class);
String currentEnvelopeMessage = IOUtils.toString(is, "UTF-8");
IOUtils.closeQuietly(is);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Inbound message: " + currentEnvelopeMessage);
}
String res = changeInboundMessage(currentEnvelopeMessage);
if (res != null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Inbound message has been changed: " + res);
}
}
res = res != null ? res : currentEnvelopeMessage;
is = IOUtils.toInputStream(res, "UTF-8");
message.setContent(InputStream.class, is);
IOUtils.closeQuietly(is);
} catch (IOException ioe) {
getLogger().warn("Unable to perform change.", ioe);
throw new RuntimeException(ioe);
}
}
}
public void handleFault(Message message) {
}
private class CachedStream extends CachedOutputStream {
public CachedStream() {
super();
}
protected void doFlush() throws IOException {
currentStream.flush();
}
protected void doClose() throws IOException {
}
protected void onWrite() throws IOException {
}
}
}
I had this problem as well today. After much weeping and gnashing of teeth, I was able to alter the StreamInterceptor class in the configuration_interceptor demo that comes with the CXF source:
OutputStream os = message.getContent(OutputStream.class);
CachedStream cs = new CachedStream();
message.setContent(OutputStream.class, cs);
message.getInterceptorChain().doIntercept(message);
try {
cs.flush();
CachedOutputStream csnew = (CachedOutputStream) message.getContent(OutputStream.class);
String soapMessage = IOUtils.toString(csnew.getInputStream());
...
The soapMessage variable will contain the complete SOAP message. You should be able to manipulate the soap message, flush it to an output stream and do a message.setContent(OutputStream.class... call to put your modifications on the message. This comes with no warranty, since I'm pretty new to CXF myself!
Note: CachedStream is a private class in the StreamInterceptor class. Don't forget to configure your interceptor to run in the PRE_STREAM phase so that the SOAP interceptors have a chance to write the SOAP message.
Following is able to bubble up server side exceptions. Use of os.close() instead of IOUtils.closeQuietly(os) in previous solution is also able to bubble up exceptions.
public class OutInterceptor extends AbstractPhaseInterceptor<Message> {
public OutInterceptor() {
super(Phase.PRE_STREAM);
addBefore(StaxOutInterceptor.class.getName());
}
public void handleMessage(Message message) {
OutputStream os = message.getContent(OutputStream.class);
CachedOutputStream cos = new CachedOutputStream();
message.setContent(OutputStream.class, cos);
message.getInterceptorChain.aad(new PDWSOutMessageChangingInterceptor(os));
}
}
public class OutMessageChangingInterceptor extends AbstractPhaseInterceptor<Message> {
private OutputStream os;
public OutMessageChangingInterceptor(OutputStream os){
super(Phase.PRE_STREAM_ENDING);
addAfter(StaxOutEndingInterceptor.class.getName());
this.os = os;
}
public void handleMessage(Message message) {
try {
CachedOutputStream csnew = (CachedOutputStream) message .getContent(OutputStream.class);
String currentEnvelopeMessage = IOUtils.toString( csnew.getInputStream(), (String) message.get(Message.ENCODING));
csnew.flush();
IOUtils.closeQuietly(csnew);
String res = changeOutboundMessage(currentEnvelopeMessage);
res = res != null ? res : currentEnvelopeMessage;
InputStream replaceInStream = IOUtils.tolnputStream(res, (String) message.get(Message.ENCODING));
IOUtils.copy(replaceInStream, os);
replaceInStream.close();
IOUtils.closeQuietly(replaceInStream);
message.setContent(OutputStream.class, os);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
Good example for replacing outbound soap content based on this
package kz.bee.bip;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor;
import org.apache.cxf.io.CachedOutputStream;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
public class SOAPOutboundInterceptor extends AbstractPhaseInterceptor<Message> {
public SOAPOutboundInterceptor() {
super(Phase.PRE_STREAM);
addBefore(SoapPreProtocolOutInterceptor.class.getName());
}
public void handleMessage(Message message) {
boolean isOutbound = false;
isOutbound = message == message.getExchange().getOutMessage()
|| message == message.getExchange().getOutFaultMessage();
if (isOutbound) {
OutputStream os = message.getContent(OutputStream.class);
CachedStream cs = new CachedStream();
message.setContent(OutputStream.class, cs);
message.getInterceptorChain().doIntercept(message);
try {
cs.flush();
IOUtils.closeQuietly(cs);
CachedOutputStream csnew = (CachedOutputStream) message.getContent(OutputStream.class);
String currentEnvelopeMessage = IOUtils.toString(csnew.getInputStream(), "UTF-8");
csnew.flush();
IOUtils.closeQuietly(csnew);
/* here we can set new data instead of currentEnvelopeMessage*/
InputStream replaceInStream = IOUtils.toInputStream(currentEnvelopeMessage, "UTF-8");
IOUtils.copy(replaceInStream, os);
replaceInStream.close();
IOUtils.closeQuietly(replaceInStream);
os.flush();
message.setContent(OutputStream.class, os);
IOUtils.closeQuietly(os);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
public void handleFault(Message message) {
}
private static class CachedStream extends CachedOutputStream {
public CachedStream() {
super();
}
protected void doFlush() throws IOException {
currentStream.flush();
}
protected void doClose() throws IOException {
}
protected void onWrite() throws IOException {
}
}
}
a better way would be to modify the message using the DOM interface, you need to add the SAAJOutInterceptor first (this might have a performance hit for big requests) and then your custom interceptor that is executed in phase USER_PROTOCOL
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Node;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
abstract public class SoapNodeModifierInterceptor extends AbstractSoapInterceptor {
SoapNodeModifierInterceptor() { super(Phase.USER_PROTOCOL); }
#Override public void handleMessage(SoapMessage message) throws Fault {
try {
if (message == null) {
return;
}
SOAPMessage sm = message.getContent(SOAPMessage.class);
if (sm == null) {
throw new RuntimeException("You must add the SAAJOutInterceptor to the chain");
}
modifyNodes(sm.getSOAPBody());
} catch (SOAPException e) {
throw new RuntimeException(e);
}
}
abstract void modifyNodes(Node node);
}
this one's working for me. It's based on StreamInterceptor class from configuration_interceptor example in Apache CXF samples.
It's in Scala instead of Java but the conversion is straightforward.
I tried to add comments to explain what's happening (as far as I understand).
import java.io.OutputStream
import org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor
import org.apache.cxf.helpers.IOUtils
import org.apache.cxf.io.CachedOutputStream
import org.apache.cxf.message.Message
import org.apache.cxf.phase.AbstractPhaseInterceptor
import org.apache.cxf.phase.Phase
// java note: base constructor call is hidden at the end of class declaration
class StreamInterceptor() extends AbstractPhaseInterceptor[Message](Phase.PRE_STREAM) {
// java note: put this into the constructor after calling super(Phase.PRE_STREAM);
addBefore(classOf[SoapPreProtocolOutInterceptor].getName)
override def handleMessage(message: Message) = {
// get original output stream
val osOrig = message.getContent(classOf[OutputStream])
// our output stream
val osNew = new CachedOutputStream
// replace it with ours
message.setContent(classOf[OutputStream], osNew)
// fills the osNew instead of osOrig
message.getInterceptorChain.doIntercept(message)
// flush before getting content
osNew.flush()
// get filled content
val content = IOUtils.toString(osNew.getInputStream, "UTF-8")
// we got the content, we may close our output stream now
osNew.close()
// modified content
val modifiedContent = content.replace("a-string", "another-string")
// fill original output stream
osOrig.write(modifiedContent.getBytes("UTF-8"))
// flush before set
osOrig.flush()
// replace with original output stream filled with our modified content
message.setContent(classOf[OutputStream], osOrig)
}
}

CompletableFuture to make webservice calls and save when everything is done

I have a list of sessions that I have to call a webservice to set some property on each session.
I am trying to call webservice using async process and use completablefuture for it so that when it is all done, I can save them all in db.
How can I do this? So far, my code is as follows, it doesn't work.
sessions.stream()
.forEach(s -> CompletableFuture.runAsync(() -> webServiceCall(s), executor));
sessionService.saveAll(sessions);
EDIT:
I came up with this solution, not sure if this is the correct way of doing it.
List<CompletableFuture<Void>> futures = sessions.stream()
.map(s -> CompletableFuture.runAsync(() -> webServiceCall(s), executor))
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.join();
sessionService.saveAll(sessions);
I am using join to make sure it waits for response to return before saving sessions
In short - all you need something like this -
CompletableFuture.supplyAsync(this::supplySomething, ex).thenAccept(this::consumer);
You need a method that will call in a executor (threadpool). In my case my pool size is 100. Next you need to call your supplier as many times as you want.
Each call to 'supplier' will create one task. I'm creating 10000 tasks. Each of them will run in parallel and each of them, upon completion, will call my 'consumer'.
Your supplier should return some sort of object which holds response from webservice. This object will then become the parameter of your 'consumer' method.
You might want to kill the pool after (or in middle) everything is done.
See an example below -
package com.sanjeev.java8.thread;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Caller {
public static ExecutorService ex = Executors.newFixedThreadPool(100);
public static void main(String[] args) throws InterruptedException {
Caller caller = new Caller();
caller.start();
ex.shutdown();
ex.awaitTermination(10, TimeUnit.MINUTES);
}
private void start() {
for (int i = 0; i < 10000; i++) {
CompletableFuture.supplyAsync(this::supplySomething, ex).thenAccept(this::consumer);
}
}
private int supplySomething() {
try {
URL url = new URL("http://www.mywebservice.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.connect();
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.write("supply-some-data".getBytes());
}
Reader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
for (int c; (c = in.read()) >= 0;) {
System.out.print((char) c);
}
in.close();
// return the response code. I'm return 'int', you should return some sort of object.
return 200;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public void consumer(Integer i) {
// This parameter should be of type 'your object' that supplier returned.
// I got the response; add it in the list or whatever....
}
}
Another example that might suits your need better -
public class Caller2 {
public static ExecutorService ex = Executors.newFixedThreadPool(2);
private static Iterator<String> addresses = Stream.of("www.google.com", "www.yahoo.com", "www.abc.com").collect(Collectors.toList()).iterator();
private static ArrayList<String> results = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
Caller2 caller = new Caller2();
caller.start();
ex.shutdown();
ex.awaitTermination(1, TimeUnit.HOURS);
System.out.println(results);
}
private void start() {
while (addresses.hasNext()) {
CompletableFuture.supplyAsync(this::supplyURL, ex).thenAccept(this::consumer);
}
}
private String supplyURL() {
String url = addresses.next();
// call this URL and return response;
return "Success";
}
public void consumer(String result) {
results.add(result);
}

Resources