Implementing different destinations in applications on the Tomcat server - spring

Earlier this year I developed an implementation of the SAP JCO CustomDestinationProvider for one of my Spring MVC tomcat applications. In my application, I use this implementation to call a BAPI in my SAP R/3 system to retrieve data.
I am now working on a second Spring MVC tomcat application that I want to have call a BAPI in my SAP R/3 system to retrieve data. It will be a different BAPI that I will be calling, thus it will be different data that I will be retrieving. Since this is a different application calling a different BAPI, I want to use a different SAP system user in my configurations. This new application will be running on the same physical tomcat server as the first application.
My question is should I develop another implementation of the SAP JCO CustomDestinationProvider for this new application or should I somehow reuse the first implementation? If the answer is that I should develop another implementation for this new application, I would expect then that I would develop another implementation for each new Spring MVC tomcat application that I develop that needs to talk to SAP. Is this correct thinking?
If I do a different implementation for this new application of mine, should I be using the same destination name in the code, or should I use a different name?
Below is the code for my first implementation of CustomDestinationDataProvider:
public class CustomDestinationDataProvider {
public class MyDestinationDataProvider implements DestinationDataProvider {
private DestinationDataEventListener eL;
private HashMap<String, Properties> secureDBStorage = new HashMap<String, Properties>();
public Properties getDestinationProperties(String destinationName) {
try {
Properties p = secureDBStorage.get(destinationName);
if(p!=null) {
if(p.isEmpty())
throw new DataProviderException(DataProviderException.Reason.INVALID_CONFIGURATION, "destination configuration is incorrect", null);
return p;
}
return null;
} catch(RuntimeException re) {
throw new DataProviderException(DataProviderException.Reason.INTERNAL_ERROR, re);
}
}
public void setDestinationDataEventListener(DestinationDataEventListener eventListener) {
this.eL = eventListener;
}
public boolean supportsEvents() {
return true;
}
public void changeProperties(String destName, Properties properties) {
synchronized(secureDBStorage) {
if(properties==null) {
if(secureDBStorage.remove(destName)!=null)
eL.deleted(destName);
} else {
secureDBStorage.put(destName, properties);
eL.updated(destName); // create or updated
}
}
}
}
public ArrayList<String> executeSAPCall(Properties connectProperties, ArrayList<String> partnumbers) throws Exception {
String destName = "ABAP_AS";
SAPDAO sapDAO = new SAPDAO();
ArrayList<MaterialBean> searchResults = new ArrayList<MaterialBean>();
MyDestinationDataProvider myProvider = new MyDestinationDataProvider();
boolean destinationDataProviderRegistered = com.sap.conn.jco.ext.Environment.isDestinationDataProviderRegistered();
JCoDestination dest;
try {
if (!destinationDataProviderRegistered) {
com.sap.conn.jco.ext.Environment.registerDestinationDataProvider(myProvider);
myProvider.changeProperties(destName, connectProperties);
}
} catch(IllegalStateException providerAlreadyRegisteredException) {
logger.error("executeSAPCall: providerAlreadyRegisteredException!");
}
try {
dest = JCoDestinationManager.getDestination(destName);
searchResults = sapDAO.searchSAP(dest, partnumbers);
} catch(JCoException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return searchResults;
}
}
If the answer is that I should not need to implement another CustomDestinationDataProvider for my second application, what other considerations do I need to keep in mind?

You can only register one DestinationDataProvider so the one you set must be able to handle both (or more) different connections. In order to do this, you need unique names for each connection, i.e. destName can't be the fixed value ABAP_AS, you need to create one for each connection.
Your current implementation of the provider looks good for me, but your method when calling the RFC is mixing the creation of the connection and the actual RFC-calling too much in my eyes. IMHO you should separate the former into its own method, so you can call it from other parts of your application to e.g. do other things than RFC-calling.

I've figured it out! I discovered two different ways to implement CustomDestinationDataProvider so that I could use multiple destinations.
Something that I did that helped out with both of my different solutions was change out the method in CustomDestinationDataProvider that instantiates the MyDestinationDataProvider inner class so that instead of returning ArrayList, it returns JCoDestination. I changed the name of this method from executeSAPCall to getDestination.
The first way that I discovered that allowed me to use multiple destinations, successfully changing out destinations, was to introduce a class variable for MyDestinationDataProvider so that I could keep my instantiated version. Please note that for this solution, the CustomDestinationDataProvider class is still embedded within my java application code.
I found that this solution only worked for one application. I was not able to use this mechanism in multiple applications on the same tomcat server, but at least I was finally able to successfully switch destinations. Here is the code for CustomDestinationDataProvider.java for this first solution:
public class CustomDestinationDataProvider {
private MyDestinationDataProvider gProvider; // class version of MyDestinationDataProvider
public class MyDestinationDataProvider implements DestinationDataProvider {
private DestinationDataEventListener eL;
private HashMap<String, Properties> secureDBStorage = new HashMap<String, Properties>();
public Properties getDestinationProperties(String destinationName) {
try {
Properties p = secureDBStorage.get(destinationName);
if(p!=null) {
if(p.isEmpty())
throw new DataProviderException(DataProviderException.Reason.INVALID_CONFIGURATION, "destination configuration is incorrect", null);
return p;
}
return null;
} catch(RuntimeException re) {
System.out.println("getDestinationProperties: Exception detected!!! message = " + re.getMessage());
throw new DataProviderException(DataProviderException.Reason.INTERNAL_ERROR, re);
}
}
public void setDestinationDataEventListener(DestinationDataEventListener eventListener) {
this.eL = eventListener;
}
public boolean supportsEvents() {
return true;
}
public void changeProperties(String destName, Properties properties) {
synchronized(secureDBStorage) {
if(properties==null) {
if(secureDBStorage.remove(destName)!=null) {
eL.deleted(destName);
}
} else {
secureDBStorage.put(destName, properties);
eL.updated(destName); // create or updated
}
}
}
}
public JCoDestination getDestination(String destName, Properties connectProperties) {
MyDestinationDataProvider myProvider = new MyDestinationDataProvider();
boolean destinationDataProviderRegistered = com.sap.conn.jco.ext.Environment.isDestinationDataProviderRegistered();
if (!destinationDataProviderRegistered) {
try {
com.sap.conn.jco.ext.Environment.registerDestinationDataProvider(myProvider);
gProvider = myProvider; // save our destination data provider in the class var
} catch(IllegalStateException providerAlreadyRegisteredException) {
throw new Error(providerAlreadyRegisteredException);
}
} else {
myProvider = gProvider; // get the destination data provider from the class var.
}
myProvider.changeProperties(destName, connectProperties);
JCoDestination dest = null;
try {
dest = JCoDestinationManager.getDestination(destName);
} catch(JCoException e) {
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
}
return dest;
}
}
This is the code in my servlet class that I use to instantiate and call CustomDestinationDataProvider within my application code:
CustomDestinationDataProvider cddp = new CustomDestinationDataProvider();
SAPDAO sapDAO = new SAPDAO();
Properties p1 = getProperties("SAPSystem01");
Properties p2 = getProperties("SAPSystem02");
try {
JCoDestination dest = cddp.getDestination("SAP_R3_USERID_01", p1); // establish the first destination
sapDAO.searchEmployees(dest, searchCriteria); // call the first BAPI
dest = cddp.getDestination("SAP_R3_USERID_02", p2); // establish the second destination
sapDAO.searchAvailability(dest); // call the second BAPI
} catch (Exception e) {
e.printStackTrace();
}
Again, this solution only works within one application. If you implement this code directly into more than one application, the first app that calls this code gets the resource and the other one will error out.
The second solution that I came up with allows multiple java applications to use the CustomDestinationDataProvider class at the same time. I broke the CustomDestinationDataProvider class out of my application code and created a separate java spring application for it (not a web application) for the purpose of creating a jar. I then transformed the MyDestinationDataProvider inner class into a singleton. Here's the code for the singleton version of CustomDestinationDataProvider:
public class CustomDestinationDataProvider {
public static class MyDestinationDataProvider implements DestinationDataProvider {
////////////////////////////////////////////////////////////////////
// The following lines convert MyDestinationDataProvider into a singleton. Notice
// that the MyDestinationDataProvider class has now been declared as static.
private static MyDestinationDataProvider myDestinationDataProvider = null;
private MyDestinationDataProvider() {
}
public static MyDestinationDataProvider getInstance() {
if (myDestinationDataProvider == null) {
myDestinationDataProvider = new MyDestinationDataProvider();
}
return myDestinationDataProvider;
}
////////////////////////////////////////////////////////////////////
private DestinationDataEventListener eL;
private HashMap<String, Properties> secureDBStorage = new HashMap<String, Properties>();
public Properties getDestinationProperties(String destinationName) {
try {
Properties p = secureDBStorage.get(destinationName);
if(p!=null) {
if(p.isEmpty())
throw new DataProviderException(DataProviderException.Reason.INVALID_CONFIGURATION, "destination configuration is incorrect", null);
return p;
}
return null;
} catch(RuntimeException re) {
throw new DataProviderException(DataProviderException.Reason.INTERNAL_ERROR, re);
}
}
public void setDestinationDataEventListener(DestinationDataEventListener eventListener) {
this.eL = eventListener;
}
public boolean supportsEvents() {
return true;
}
public void changeProperties(String destName, Properties properties) {
synchronized(secureDBStorage) {
if(properties==null) {
if(secureDBStorage.remove(destName)!=null) {
eL.deleted(destName);
}
} else {
secureDBStorage.put(destName, properties);
eL.updated(destName); // create or updated
}
}
}
}
public JCoDestination getDestination(String destName, Properties connectProperties) throws Exception {
MyDestinationDataProvider myProvider = MyDestinationDataProvider.getInstance();
boolean destinationDataProviderRegistered = com.sap.conn.jco.ext.Environment.isDestinationDataProviderRegistered();
if (!destinationDataProviderRegistered) {
try {
com.sap.conn.jco.ext.Environment.registerDestinationDataProvider(myProvider);
} catch(IllegalStateException providerAlreadyRegisteredException) {
throw new Error(providerAlreadyRegisteredException);
}
}
myProvider.changeProperties(destName, connectProperties);
JCoDestination dest = null;
try {
dest = JCoDestinationManager.getDestination(destName);
} catch(JCoException ex) {
ex.printStackTrace();
throw ex;
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
return dest;
}
}
After putting this code into the jar file application and creating the jar file (I call it JCOConnector.jar), I put the jar file on the shared library classpath of my tomcat server and restarted the tomcat server. In my case, this was /opt/tomcat/shared/lib. Check your /opt/tomcat/conf/catalina.properties file for the shared.loader line for the location of your shared library classpath. Mine looks like this:
shared.loader=\
${catalina.home}/shared/lib\*.jar,${catalina.home}/shared/lib
I also put a copy of this jar file in the "C:\Users\userid\Documents\jars" folder on my workstation so that the test application code could see the code in the jar and compile. I then referenced this copy of the jar file in my pom.xml file in both of my test applications:
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>jcoconnector</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>C:\Users\userid\Documents\jars\JCOConnector.jar</systemPath>
</dependency>
After adding this to the pom.xml file, I right clicked on each project, selected Maven -> Update Project..., and I then right clicked again on each project and selected 'Refresh'. Something very important that I learned was to not add a copy of JCOConnector.jar directly to either of my test projects. The reason for this is because I want the code from the jar file in /opt/tomcat/shared/lib/JCOConnector.jar to be used. I then built and deployed each of my test apps to the tomcat server.
The code that calls my JCOConnector.jar shared library in my first test application looks like this:
CustomDestinationDataProvider cddp = new CustomDestinationDataProvider();
JCoDestination dest = null;
SAPDAO sapDAO = new SAPDAO();
Properties p1 = getProperties("SAPSystem01");
try {
dest = cddp.getDestination("SAP_R3_USERID_01", p1);
sapDAO.searchEmployees(dest);
} catch (Exception ex) {
ex.printStackTrace();
}
The code in my second test application that calls my JCOConnector.jar shared library looks like this:
CustomDestinationDataProvider cddp = new CustomDestinationDataProvider();
JCoDestination dest = null;
SAPDAO sapDAO = new SAPDAO();
Properties p2 = getProperties("SAPSystem02");
try {
dest = cddp.getDestination("SAP_R3_USERID_02", p2);
sapDAO.searchAvailability(dest);
} catch (Exception ex) {
ex.printStackTrace();
}
I know that I've left out a lot of the steps involved in first getting the SAP JCO 3 library installed on your workstation and server. I do hope that this helps out at least one other person of getting over the hill of trying to get multiple spring mvc java spplications talking to SAP on the same server.

Related

How to run Quarkus programatically in Test mode

I am trying to run acceptance tests with concordion fixtures in a quarkus project. Concordion does not work with Junit5 so I am using its original #Run(ConcordionRunner.class).
I am creating a superclass to start my quarkus application before tests like that:
#RunWith(ConcordionRunner.class)
public abstract class AbstractFixture {
public static RunningQuarkusApplication application;
protected static RequestSpecification server;
protected AbstractFixture() {
setUp();
}
public void setUp() {
if(application == null) {
startApplication();
server = new RequestSpecBuilder()
.setPort(8081)
.setContentType(ContentType.JSON)
.build();
}
}
private void startApplication() {
try {
PathsCollection.Builder rootBuilder = PathsCollection.builder();
Path testClassLocation = PathTestHelper.getTestClassesLocation(getClass());
rootBuilder.add(testClassLocation);
final Path appClassLocation = PathTestHelper.getAppClassLocationForTestLocation(
testClassLocation.toString());
rootBuilder.add(appClassLocation);
application = QuarkusBootstrap.builder()
.setIsolateDeployment(false)
.setMode(QuarkusBootstrap.Mode.TEST)
.setProjectRoot(Paths.get("").normalize().toAbsolutePath())
.setApplicationRoot(rootBuilder.build())
.build()
.bootstrap()
.createAugmentor()
.createInitialRuntimeApplication()
.run();
} catch (BindException e) {
e.printStackTrace();
System.out.println("Address already in use - which is fine!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
The code above is working but I can't change the default port 8081 to any other.
If I print the config property in my Test class like below, it prints the port correctly, but quarkus is not running on it:
public class HelloFixture extends AbstractFixture {
public String getGreeting() {
Response response = given(server).when().get("/hello");
System.out.println("Config[port]: " + application.getConfigValue("quarkus.http.port", String.class));
return response.asString();
}
}
How can I specify the configuration file or property programatically before run?
I found the answer. At first, I was referencing the wrong property "quarkus.http.port" instead of "quarkus.http.test-port".
Despite that, I found the way to override properties before run:
...
StartupAction action = QuarkusBootstrap.builder()
.setIsolateDeployment(false)
.setMode(QuarkusBootstrap.Mode.TEST)
.setProjectRoot(Paths.get("").normalize().toAbsolutePath())
.setApplicationRoot(rootBuilder.build())
.build()
.bootstrap()
.createAugmentor()
.createInitialRuntimeApplication();
action.overrideConfig(getConfigOverride());
application = action.run();
...
private Map<String, String> getConfigOverride() {
Map<String, String> config = new HashMap<>();
config.put("quarkus.http.test-port", "18082");
return config;
}

Micrometer - WebMvcTagsContributor not adding custom tags

I'm trying to add custom tags - the path variables and their values from each request - to each metric micrometer generates. I'm using spring-boot with java 16.
From my research i've found that creating a bean of type WebMvcTagsContributor alows me to do just that.
This is the code
public class CustomWebMvcTagsContributor implements WebMvcTagsContributor {
private static int PRINT_ERROR_COUNTER = 0;
#Override
public Iterable<Tag> getTags(HttpServletRequest request, HttpServletResponse response,
Object handler,
Throwable exception) {
return Tags.of(getAllTags(request));
}
private static List<Tag> getAllTags(HttpServletRequest request) {
Object attributesMapObject = request.getAttribute(View.PATH_VARIABLES);
if (isNull(attributesMapObject)) {
attributesMapObject = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (isNull(attributesMapObject)) {
attributesMapObject = extractPathVariablesFromURI(request);
}
}
if (nonNull(attributesMapObject)) {
return getPathVariablesTags(attributesMapObject);
}
return List.of();
}
private static Object extractPathVariablesFromURI(HttpServletRequest request) {
Long currentUserId = SecurityUtils.getCurrentUserId().orElse(null);
try {
URI uri = new URI(request.getRequestURI());
String path = uri.getPath(); //get the path
UriTemplate uriTemplate = new UriTemplate((String) request.getAttribute(
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); //create template
return uriTemplate.match(path); //extract values form template
} catch (Exception e) {
log.warn("[Error on 3rd attempt]", e);
}
return null;
}
private static List<Tag> getPathVariablesTags(Object attributesMapObject) {
try {
Long currentUserId = SecurityUtils.getCurrentUserId().orElse(null);
if (nonNull(attributesMapObject)) {
var attributesMap = (Map<String, Object>) attributesMapObject;
List<Tag> tags = attributesMap.entrySet().stream()
.map(stringObjectEntry -> Tag.of(stringObjectEntry.getKey(),
String.valueOf(stringObjectEntry.getValue())))
.toList();
log.warn("[CustomTags] [{}]", CommonUtils.toJson(tags));
return tags;
}
} catch (Exception e) {
if (PRINT_ERROR_COUNTER < 5) {
log.error("[Error while getting attributes map object]", e);
PRINT_ERROR_COUNTER++;
}
}
return List.of();
}
#Override
public Iterable<Tag> getLongRequestTags(HttpServletRequest request, Object handler) {
return null;
}
}
#Bean
public WebMvcTagsContributor webMvcTagsContributor() {
return new CustomWebMvcTagsContributor();
}
In order to test this, i've created a small spring boot app, added an endpoint to it. It works just fine.
The problem is when I add this code to the production app.
The metrics generates are the default ones and i can't figure out why.
What can I check to see why the tags are not added?
local test project
http_server_requests_seconds_count {exception="None", method="GET",id="123",outcome="Success",status="200",test="test",uri="/test/{id}/compute/{test}",)1.0
in prod - different (& bigger) app
http_server_requests_seconds_count {exception="None", method="GET",outcome="Success",status="200",uri="/api/{something}/test",)1.0
What i've tried and didn't work
Created a bean that implemented WebMvcTagsProvider - this one had an odd behaviour - it wasn't creating metrics for endpoints that had path variables in the path - though in my local test project it worked as expected
I added that log there in order to see what the extra tags are but doesn't seem to reach there as i don't see anything in the logs - i know, you might say that the current user id stops it, but it's not that.

List Files from Templates Directory in Spring Boot

I would like to generate a blog posts overview. For that I want to read the html files from a folder inside the templates folder in the resources folder where Spring Boot stores its templates.
I tried that but it doesnt return an error but also list no files.
What is the way to go here?
Thanks
#Controller
public class Route {
#Autowired
private ResourceLoader resourceLoader;
#RequestMapping("/")
public String home() throws IOException {
final String path = "templates/blog";
final Resource res = resourceLoader.getResource("templates/blog");
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(res.getInputStream()))) {
reader.lines().forEachOrdered(System.out::println);
}
return "blog/a";
}
}
#Controller
public class Route {
#Value("classpath:templates/blog/*")
private Resource[] resources;
#RequestMapping("/")
public String home() throws IOException {
for (final Resource res : resources) {
System.out.println(res.getFilename());
}
return "blog/a";
}
}
did the trick to me.
You should be able to achieve this using NIO2.
In order for NIO2 to work, it requires the concept of FileSystem, and one can be created from the jar URI. Then this file system can be used with Files/Paths.
The code below contains two branches - the first handles loading the files from inside Jar, the second branch - when the code runs from IDE or via "mvn spring-boot:run".
All streams are being used via try-with-resources so they will be auto-closed.
The find function starts from the top of the file system and recursively searches for html files.
public static void readFile(String location) throws URISyntaxException {
URI uri = Objects.requireNonNull(ReadFromJar.class.getClassLoader().getResource(location)).toURI();
if (uri.getScheme().equals("jar")) { //inside jar
try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { //build a new FS that represents the jar's contents
Files.find(fs.getPath("/"), 10, (path, fileAttr) -> // control the search depth (e.g. 10)
fileAttr.isRegularFile() //match only files
&& path.toString().contains("blog") //match only files in paths containing "blog"
&& path.getFileName().toString().matches(".*\\.html")) // match only html files
.forEach(ReadFromJar::printFileContent);
} catch (IOException ex) {
ex.printStackTrace();
}
}
else { //from IDE or spring-boot:run
final Path path = Paths.get(uri);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(path)) {
dirStream.forEach(ReadFromJar::printFileContent);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void printFileContent(final Path file) {
try {
System.out.println("Full path: " + file.toAbsolutePath().toString());
Files.lines(file).forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}

How can I get Start-Class programmatically?

How can I get Start-Class manifest property programmatically from a code in a jar inside BOOT-INF/lib?
I know I can get the LaunchURLClassLoader, call getResourceAsStream("META-INF/MANIFEST.MF") and then look for the property in the MANIFEST.MF but I'm not sure this is the best aproach
Your approach is fine and will probably work, but you can go one level of abstraction higher and use the Spring's Resource Loader.
Resource template = ctx.getResource("classpath:/path/to/manifest");
public class UserAgentSpringBootApplicationNameDiscoverer {
protected static final String BOOT_INF_DIR = "BOOT-INF/";
protected static final String MANIFEST_DIR = "META-INF/MANIFEST.MF";
public String getStartClassFromClassLoader(ClassLoader cl) {
URL bootInfUrl = cl.getResource(BOOT_INF_DIR);
if (bootInfUrl == null || "".equals(bootInfUrl.toString().trim())) {
return null;
}
String rootMetaInfUrl = bootInfUrl.toString().replace(BOOT_INF_DIR, MANIFEST_DIR);
InputStream is = cl.getResourceAsStream(rootMetaInfUrl.toString());
if (is == null) {
return null;
}
Manifest manifest = null;
try {
manifest = new Manifest(is);
} catch (IOException e) {
return null;
}
return manifest.getMainAttributes().getValue("Start-Class");
}
}

Spring SAML extension for multiple IDP'S

we are planning to use spring saml extension as SP into our application.
But the requirement with our application is we need to communicate with more than 1 IDP's
Could any one please provide me/direct me to the example where it uses multiple IDP's
I also would like to know spring saml extension supports what kind of IDPS like OPenAM/Ping federate/ADFs2.0 etc...
Thanks,
--Vikas
You need to have a class to maintain a list of metadatas of each Idp's - say you putting those metadatas in some list which will be shared across application by static method. I have something like below
NOTE- I am not copying all class as it is that I am having, so might came across minor issues which you should be able to resolve on your own,
public class SSOMetadataProvider {
public static List<MetadataProvider> metadataList() throws MetadataProviderException, XMLParserException, IOException, Exception {
logger.info("Starting : Loading Metadata Data for all SSO enabled companies...");
List<MetadataProvider> metadataList = new ArrayList<MetadataProvider>();
org.opensaml.xml.parse.StaticBasicParserPool parserPool = new org.opensaml.xml.parse.StaticBasicParserPool();
parserPool.initialize();
//Get XML from DB -> convertIntoInputStream -> pass below as const argument
InputStreamMetadataProvider inputStreamMetadata = null;
try {
//Getting list from DB
List companyList = someServiceClass.getAllSSOEnabledCompanyDTO();
if(companyList!=null){
for (Object obj : companyList) {
CompanyDTO companyDTO = (CompanyDTO) obj;
if (companyDTO != null && companyDTO.getCompanyid() > 0 && companyDTO.getSsoSettingsDTO()!=null && !StringUtil.isNullOrEmpty(companyDTO.getSsoSettingsDTO().getSsoMetadataXml())) {
logger.info("Loading Metadata for Company : "+companyDTO.getCompanyname()+" , companyId : "+companyDTO.getCompanyid());
inputStreamMetadata = new InputStreamMetadataProvider(companyDTO.getSsoSettingsDTO().getSsoMetadataXml());
inputStreamMetadata.setParserPool(parserPool);
inputStreamMetadata.initialize();
//ExtendedMetadataDelegateWrapper extMetadaDel = new ExtendedMetadataDelegateWrapper(inputStreamMetadata , new org.springframework.security.saml.metadata.ExtendedMetadata());
SSOMetadataDelegate extMetadaDel = new SSOMetadataDelegate(inputStreamMetadata , new org.springframework.security.saml.metadata.ExtendedMetadata()) ;
extMetadaDel.initialize();
extMetadaDel.setTrustFiltersInitialized(true);
metadataList.add(extMetadaDel);
logger.info("Loading Metadata bla bla");
}
}
}
} catch (MetadataProviderException | IOException | XMLParserException mpe){
logger.warn(mpe);
throw mpe;
}
catch (Exception e) {
logger.warn(e);
}
logger.info("Finished : Loading Metadata Data for all SSO enabled companies...");
return metadataList;
}
InputStreamMetadataProvider.java
public class InputStreamMetadataProvider extends AbstractReloadingMetadataProvider implements Serializable
{
public InputStreamMetadataProvider(String metadata) throws MetadataProviderException
{
super();
//metadataInputStream = metadata;
metadataInputStream = SSOUtil.getIdpAsStream(metadata);
}
#Override
protected byte[] fetchMetadata() throws MetadataProviderException
{
byte[] metadataBytes = metadataInputStream ;
if(metadataBytes.length>0)
return metadataBytes;
else
return null;
}
public byte[] getMetadataInputStream() {
return metadataInputStream;
}
}
SSOUtil.java
public class SSOUtil {
public static byte[] getIdpAsStream(String metadatXml) {
return metadatXml.getBytes();
}
}
After user request to fetch metadata for their company's metadata, get MetaData for entityId for each IdPs -
SSOCachingMetadataManager.java
public class SSOCachingMetadataManager extends CachingMetadataManager{
#Override
public ExtendedMetadata getExtendedMetadata(String entityID) throws MetadataProviderException {
ExtendedMetadata extendedMetadata = null;
try {
//UAT Defect Fix - org.springframework.security.saml.metadata.ExtendedMetadataDelegate cannot be cast to biz.bsite.direct.spring.app.sso.ExtendedMetadataDelegate
//List<MetadataProvider> metadataList = (List<MetadataProvider>) GenericCache.getInstance().getCachedObject("ssoMetadataList", List.class.getClassLoader());
List<MetadataProvider> metadataList = SSOMetadataProvider.metadataList();
log.info("Retrieved Metadata List from Cassendra Cache size is :"+ (metadataList!=null ? metadataList.size(): 0) );
org.opensaml.xml.parse.StaticBasicParserPool parserPool = new org.opensaml.xml.parse.StaticBasicParserPool();
parserPool.initialize();
if(metadataList!=null){
//metadataList.addAll(getAvailableProviders());
//metadataList.addAll(getProviders());
//To remove duplicate entries from list, if any
Set<MetadataProvider> hs = new HashSet<MetadataProvider> ();
hs.addAll(metadataList);
metadataList.clear();
metadataList.addAll(hs);
//setAllProviders(metadataList);
//setTrustFilterInitializedToTrue();
//refreshMetadata();
}
if(metadataList!=null && metadataList.size()>0) {
for(MetadataProvider metadataProvider : metadataList){
log.info("metadataProvider instance of ExtendedMetadataDelegate: Looking for entityId"+entityID);
SSOMetadataDelegate ssoMetadataDelegate = null;
ExtendedMetadataDelegateWrapper extMetadaDel = null;
// extMetadaDel.getDelegate()
if(metadataProvider instanceof SSOMetadataDelegate)
{ssoMetadataDelegate = (SSOMetadataDelegate) metadataProvider;
((InputStreamMetadataProvider)ssoMetadataDelegate.getDelegate()).setParserPool(parserPool);
((InputStreamMetadataProvider)ssoMetadataDelegate.getDelegate()).initialize();
ssoMetadataDelegate.initialize();
ssoMetadataDelegate.setTrustFiltersInitialized(true);
if(!isMetadataAlreadyExist(ssoMetadataDelegate))
addMetadataProvider(ssoMetadataDelegate);
extMetadaDel = new ExtendedMetadataDelegateWrapper(ssoMetadataDelegate.getDelegate() , new org.springframework.security.saml.metadata.ExtendedMetadata());
}
else
extMetadaDel = new ExtendedMetadataDelegateWrapper(metadataProvider, new org.springframework.security.saml.metadata.ExtendedMetadata());
extMetadaDel.initialize();
extMetadaDel.setTrustFiltersInitialized(true);
extMetadaDel.initialize();
refreshMetadata();
extendedMetadata = extMetadaDel.getExtendedMetadata(entityID);
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(extendedMetadata!=null)
return extendedMetadata;
else{
return super.getExtendedMetadata(entityID);
}
}
private boolean isMetadataAlreadyExist(SSOMetadataDelegate ssoMetadataDelegate) {
boolean isExist = false;
for(ExtendedMetadataDelegate item : getAvailableProviders()){
if (item.getDelegate() != null && item.getDelegate() instanceof SSOMetadataDelegate) {
SSOMetadataDelegate that = (SSOMetadataDelegate) item.getDelegate();
try {
log.info("This Entity ID: "+ssoMetadataDelegate.getMetadata()!=null ? ((EntityDescriptorImpl)ssoMetadataDelegate.getMetadata()).getEntityID() : "nullEntity"+
"That Entity ID: "+that.getMetadata()!=null ? ((EntityDescriptorImpl)that.getMetadata()).getEntityID() : "nullEntity");
EntityDescriptorImpl e = (EntityDescriptorImpl) that.getMetadata();
isExist = this.getMetadata()!=null ? ((EntityDescriptorImpl)ssoMetadataDelegate.getMetadata()).getEntityID().equals(e.getEntityID()) : false;
if(isExist)
return isExist;
} catch (MetadataProviderException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
return isExist;
}
Add entry in ur Spring bean xml
<bean id="metadata" class="pkg.path.SSOCachingMetadataManager">
<constructor-arg name="providers" value="#{ssoMetadataProvider.metadataList()}">
</constructor-arg>
<property name="RefreshCheckInterval" value="-1"/>
<property name="RefreshRequired" value="false"/>
</bean>
Let me know incase of any concerns.
I have recently configured two IDPs for Spring SAML extension. Here we should follow one basic rule. For each IDP we want to add, we have to configure one IDP provider as well as one SP provider. We should configure the providers in a MetadataManager bean, CachingMetadataManager for example. Here are some code snippets to get the idea what I am trying to say about:
public void addProvider(String providerMetadataUrl, String idpEntityId, String spEntityId, String alias) {
addIDPMetadata(providerMetadataUrl, idpEntityId, alias);
addSPMetadata(spEntityId, alias);
}
public void addIDPMetadata(String providerMetadataUrl, String idpEntityId, String alias) {
try {
if (metadata.getIDPEntityNames().contains(idpEntityId)) {
return;
}
metadata.addMetadataProvider(extendedMetadataProvider(providerMetadataUrl, alias));
} catch (MetadataProviderException e1) {
log.error("Error initializing metadata", e1);
}
}
public void addSPMetadata(String spEntityId, String alias) {
try {
if (metadata.getSPEntityNames().contains(spEntityId)) {
return;
}
MetadataGenerator generator = new MetadataGenerator();
generator.setEntityId(spEntityId);
generator.setEntityBaseURL(baseURL);
generator.setExtendedMetadata(extendedMetadata(alias));
generator.setIncludeDiscoveryExtension(true);
generator.setKeyManager(keyManager);
EntityDescriptor descriptor = generator.generateMetadata();
ExtendedMetadata extendedMetadata = generator.generateExtendedMetadata();
MetadataMemoryProvider memoryProvider = new MetadataMemoryProvider(descriptor);
memoryProvider.initialize();
MetadataProvider metadataProvider = new ExtendedMetadataDelegate(memoryProvider, extendedMetadata);
metadata.addMetadataProvider(metadataProvider);
metadata.setHostedSPName(descriptor.getEntityID());
metadata.refreshMetadata();
} catch (MetadataProviderException e1) {
log.error("Error initializing metadata", e1);
}
}
public ExtendedMetadataDelegate extendedMetadataProvider(String providerMetadataUrl, String alias)
throws MetadataProviderException {
HTTPMetadataProvider provider = new HTTPMetadataProvider(this.bgTaskTimer, httpClient, providerMetadataUrl);
provider.setParserPool(parserPool);
ExtendedMetadataDelegate delegate = new ExtendedMetadataDelegate(provider, extendedMetadata(alias));
delegate.setMetadataTrustCheck(true);
delegate.setMetadataRequireSignature(false);
return delegate;
}
private ExtendedMetadata extendedMetadata(String alias) {
ExtendedMetadata exmeta = new ExtendedMetadata();
exmeta.setIdpDiscoveryEnabled(true);
exmeta.setSignMetadata(false);
exmeta.setEcpEnabled(true);
if (alias != null && alias.length() > 0) {
exmeta.setAlias(alias);
}
return exmeta;
}
You can find all answers to your question in the Spring SAML manual.
The sample application which is included as part of the product already includes metadata for two IDPs, use it as an example.
Statement on IDPs is included in chapter 1.2:
All products supporting SAML 2.0 in Identity Provider mode (e.g. ADFS
2.0, Shibboleth, OpenAM/OpenSSO, Efecte Identity or Ping Federate) can be used with the extension.

Resources