com.helger.smpclient.bdxr1.BDXRClientReadOnly Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of peppol-smp-client Show documentation
Show all versions of peppol-smp-client Show documentation
Peppol and OASIS BDXR SMP client
/*
* Copyright (C) 2015-2024 Philip Helger
* philip[at]helger[dot]com
*
* Licensed 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 com.helger.smpclient.bdxr1;
import java.net.URI;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Locale;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.string.StringHelper;
import com.helger.peppol.sml.ISMLInfo;
import com.helger.peppol.smp.ISMPTransportProfile;
import com.helger.peppolid.CIdentifier;
import com.helger.peppolid.IDocumentTypeIdentifier;
import com.helger.peppolid.IParticipantIdentifier;
import com.helger.peppolid.IProcessIdentifier;
import com.helger.peppolid.factory.IIdentifierFactory;
import com.helger.peppolid.simple.process.SimpleProcessIdentifier;
import com.helger.security.certificate.CertificateHelper;
import com.helger.smpclient.bdxr1.marshal.BDXR1MarshallerServiceGroupType;
import com.helger.smpclient.bdxr1.marshal.BDXR1MarshallerSignedServiceMetadataType;
import com.helger.smpclient.exception.SMPClientBadRequestException;
import com.helger.smpclient.exception.SMPClientException;
import com.helger.smpclient.exception.SMPClientNotFoundException;
import com.helger.smpclient.exception.SMPClientUnauthorizedException;
import com.helger.smpclient.httpclient.AbstractGenericSMPClient;
import com.helger.smpclient.httpclient.SMPHttpResponseHandlerSigned;
import com.helger.smpclient.httpclient.SMPHttpResponseHandlerUnsigned;
import com.helger.smpclient.url.ISMPURLProvider;
import com.helger.smpclient.url.SMPDNSResolutionException;
import com.helger.xsds.bdxr.smp1.EndpointType;
import com.helger.xsds.bdxr.smp1.ProcessType;
import com.helger.xsds.bdxr.smp1.RedirectType;
import com.helger.xsds.bdxr.smp1.ServiceGroupType;
import com.helger.xsds.bdxr.smp1.ServiceInformationType;
import com.helger.xsds.bdxr.smp1.ServiceMetadataReferenceType;
import com.helger.xsds.bdxr.smp1.ServiceMetadataType;
import com.helger.xsds.bdxr.smp1.SignedServiceMetadataType;
import com.helger.xsds.xmldsig.X509DataType;
import jakarta.xml.bind.JAXBElement;
/**
* This class is used for calling the OASIS BDXR SMP v1 REST interface. This
* class only contains the read-only methods defined in the SMP specification
* and nothing else.
*
* Note: this class is also licensed under Apache 2 license, as it was not part
* of the original implementation
*
*
* @author Philip Helger
*/
public class BDXRClientReadOnly extends AbstractGenericSMPClient implements
IBDXRServiceGroupProvider,
IBDXRServiceMetadataProvider
{
public static final String URL_PART_SERVICES = "services";
private static final Logger LOGGER = LoggerFactory.getLogger (BDXRClientReadOnly.class);
/**
* Constructor with SML lookup
*
* @param aURLProvider
* The URL provider to be used. May not be null
.
* @param aParticipantIdentifier
* The participant identifier to be used. Required to build the SMP
* access URI.
* @param aSMLInfo
* The SML to be used. Required to build the SMP access URI.
* @throws SMPDNSResolutionException
* If DNS resolution failed
* @see ISMPURLProvider#getSMPURIOfParticipant(IParticipantIdentifier,
* ISMLInfo)
*/
public BDXRClientReadOnly (@Nonnull final ISMPURLProvider aURLProvider,
@Nonnull final IParticipantIdentifier aParticipantIdentifier,
@Nonnull final ISMLInfo aSMLInfo) throws SMPDNSResolutionException
{
this (aURLProvider.getSMPURIOfParticipant (aParticipantIdentifier, aSMLInfo));
}
/**
* Constructor with SML lookup
*
* @param aURLProvider
* The URL provider to be used. May not be null
.
* @param aParticipantIdentifier
* The participant identifier to be used. Required to build the SMP
* access URI.
* @param sSMLZoneName
* The SML DNS zone name to be used. Required to build the SMP access
* URI. Must end with a trailing dot (".") and may neither be
* null
nor empty to build a correct URL. May not start
* with "http://". Example: sml.peppolcentral.org.
* @throws SMPDNSResolutionException
* if DNS resolution failed
* @see ISMPURLProvider#getSMPURIOfParticipant(IParticipantIdentifier, String)
*/
public BDXRClientReadOnly (@Nonnull final ISMPURLProvider aURLProvider,
@Nonnull final IParticipantIdentifier aParticipantIdentifier,
@Nonnull @Nonempty final String sSMLZoneName) throws SMPDNSResolutionException
{
this (aURLProvider.getSMPURIOfParticipant (aParticipantIdentifier, sSMLZoneName));
}
/**
* Constructor with a direct SMP URL.
* Remember: must be HTTP and using port 80 only!
*
* @param aSMPHost
* The address of the SMP service. Must be port 80 and basic http only
* (no https!). Example: http://smpcompany.company.org
*/
public BDXRClientReadOnly (@Nonnull final URI aSMPHost)
{
super (aSMPHost, false);
}
/**
* Returns a service group. A service group references to the service
* metadata. This is a specification compliant method.
*
* @param aServiceGroupID
* The service group id corresponding to the service group which one
* wants to get.
* @return The service group. Never null
.
* @throws SMPClientException
* in case something goes wrong
* @throws SMPClientUnauthorizedException
* A HTTP Forbidden was received, should not happen.
* @throws SMPClientNotFoundException
* The service group id did not exist.
* @throws SMPClientBadRequestException
* The request was not well formed.
* @see #getServiceGroupOrNull(IParticipantIdentifier)
*/
@Nonnull
public ServiceGroupType getServiceGroup (@Nonnull final IParticipantIdentifier aServiceGroupID) throws SMPClientException
{
ValueEnforcer.notNull (aServiceGroupID, "ServiceGroupID");
final String sURI = getSMPHostURI () + aServiceGroupID.getURIPercentEncoded ();
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("BDXRClient getServiceGroup@" + sURI);
final HttpGet aRequest = new HttpGet (sURI);
final BDXR1MarshallerServiceGroupType aMarshaller = new BDXR1MarshallerServiceGroupType ();
aMarshaller.setUseSchema (isXMLSchemaValidation ());
customizeMarshaller (aMarshaller);
final ServiceGroupType ret = executeGenericRequest (aRequest, new SMPHttpResponseHandlerUnsigned <> (aMarshaller));
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Received response: " + ret);
return ret;
}
@Nullable
public ServiceGroupType getServiceGroupOrNull (@Nonnull final IParticipantIdentifier aServiceGroupID) throws SMPClientException
{
try
{
return getServiceGroup (aServiceGroupID);
}
catch (final SMPClientNotFoundException ex)
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Found no ServiceGroup");
return null;
}
}
/**
* Extract all parsable document types from the passed Service group.
*
* @param aSG
* The service group to parse. May be null
.
* @param aIdentifierFactory
* The identifier factory to be used. May not be null
.
* @param aUnhandledHrefHandler
* An optional consumer for Hrefs that could not be parsed into a
* document type identifier. May be null
.
* @return Never null
but a maybe empty list.
* @since 8.0.4
*/
@Nonnull
public static ICommonsList getAllDocumentTypes (@Nullable final ServiceGroupType aSG,
@Nonnull final IIdentifierFactory aIdentifierFactory,
@Nullable final Consumer aUnhandledHrefHandler)
{
ValueEnforcer.notNull (aIdentifierFactory, "IdentifierFactory");
final ICommonsList ret = new CommonsArrayList <> ();
if (aSG != null && aSG.getParticipantIdentifier () != null && aSG.getServiceMetadataReferenceCollection () != null)
{
final String sPathStart = "/" +
CIdentifier.getURIEncoded (aSG.getParticipantIdentifier ()) +
'/' +
URL_PART_SERVICES +
'/';
for (final ServiceMetadataReferenceType aSMR : aSG.getServiceMetadataReferenceCollection ()
.getServiceMetadataReference ())
{
final String sOriginalHref = aSMR.getHref ();
// Decoded href is important for unification
final String sHref = CIdentifier.createPercentDecoded (sOriginalHref);
boolean bSuccess = false;
// Case insensitive "indexOf" here
final int nPathStart = StringHelper.getIndexOfIgnoreCase (sHref, sPathStart, Locale.US);
if (nPathStart >= 0)
{
final String sDocType = sHref.substring (nPathStart + sPathStart.length ());
final IDocumentTypeIdentifier aDocType = aIdentifierFactory.parseDocumentTypeIdentifier (sDocType);
if (aDocType != null)
{
// Found a document type
ret.add (aDocType);
bSuccess = true;
}
}
if (!bSuccess)
{
// Failed to parse as doc type
if (aUnhandledHrefHandler != null)
aUnhandledHrefHandler.accept (sOriginalHref);
}
}
}
return ret;
}
/**
* Gets a signed service metadata object given by its service group id and its
* document type. This is a specification compliant method.
*
* @param aServiceGroupID
* The service group id of the service metadata to get. May not be
* null
.
* @param aDocumentTypeID
* The document type of the service metadata to get. May not be
* null
.
* @return A signed service metadata object. Never null
.
* @throws SMPClientException
* in case something goes wrong
* @throws SMPClientUnauthorizedException
* A HTTP Forbidden was received, should not happen.
* @throws SMPClientNotFoundException
* The service group id or document type did not exist.
* @throws SMPClientBadRequestException
* The request was not well formed.
* @see #getServiceMetadataOrNull(IParticipantIdentifier,
* IDocumentTypeIdentifier)
* @since v8.0.0
*/
@Nonnull
public SignedServiceMetadataType getServiceMetadata (@Nonnull final IParticipantIdentifier aServiceGroupID,
@Nonnull final IDocumentTypeIdentifier aDocumentTypeID) throws SMPClientException
{
ValueEnforcer.notNull (aServiceGroupID, "ServiceGroupID");
ValueEnforcer.notNull (aDocumentTypeID, "DocumentTypeID");
final String sURI = getSMPHostURI () +
aServiceGroupID.getURIPercentEncoded () +
"/" +
URL_PART_SERVICES +
"/" +
aDocumentTypeID.getURIPercentEncoded ();
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("BDXRClient getServiceRegistration@" + sURI);
final boolean bXSDValidation = isXMLSchemaValidation ();
final boolean bVerifySignature = isVerifySignature ();
final boolean bSecureValidation = isSecureValidation ();
final KeyStore aTrustStore = getTrustStore ();
if (bVerifySignature && aTrustStore == null)
LOGGER.error ("BDXR SMP client Verify Signature is enabled, but no TrustStore is provided. This will not work.");
SignedServiceMetadataType aMetadata;
{
final HttpGet aRequest = new HttpGet (sURI);
final BDXR1MarshallerSignedServiceMetadataType aMarshaller = new BDXR1MarshallerSignedServiceMetadataType ();
aMarshaller.setUseSchema (bXSDValidation);
customizeMarshaller (aMarshaller);
// Deal with signed responses
final SMPHttpResponseHandlerSigned aResponseHandler = new SMPHttpResponseHandlerSigned <> (aMarshaller,
aTrustStore);
aResponseHandler.setVerifySignature (bVerifySignature);
aResponseHandler.setSecureValidation (bSecureValidation);
// Main execution
aMetadata = executeGenericRequest (aRequest, aResponseHandler);
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Received response: " + aMetadata);
}
// If the Redirect element is present, then follow 1 redirect.
if (isFollowSMPRedirects ())
{
if (aMetadata.getServiceMetadata () != null && aMetadata.getServiceMetadata ().getRedirect () != null)
{
final RedirectType aRedirect = aMetadata.getServiceMetadata ().getRedirect ();
// Follow the redirect
LOGGER.info ("Following a redirect from '" + sURI + "' to '" + aRedirect.getHref () + "'");
final HttpGet aRequest = new HttpGet (aRedirect.getHref ());
// Create a new Marshaller to make sure customization is easy
final BDXR1MarshallerSignedServiceMetadataType aMarshaller = new BDXR1MarshallerSignedServiceMetadataType ();
aMarshaller.setUseSchema (bXSDValidation);
customizeMarshaller (aMarshaller);
// Deal with signed responses
final SMPHttpResponseHandlerSigned aResponseHandler = new SMPHttpResponseHandlerSigned <> (aMarshaller,
aTrustStore);
aResponseHandler.setVerifySignature (bVerifySignature);
aResponseHandler.setSecureValidation (bSecureValidation);
// Main execution
aMetadata = executeGenericRequest (aRequest, aResponseHandler);
// Check that the certificateUID is correct
boolean bCertificateSubjectFound = false;
for (final Object aObj : aMetadata.getSignature ().getKeyInfo ().getContent ())
if (aObj instanceof JAXBElement >)
{
final Object aInfoValue = ((JAXBElement >) aObj).getValue ();
if (aInfoValue instanceof X509DataType)
{
final X509DataType aX509Data = (X509DataType) aInfoValue;
if (containsRedirectSubject (aX509Data, aRedirect.getCertificateUID ()))
{
bCertificateSubjectFound = true;
break;
}
}
}
if (!bCertificateSubjectFound)
throw new SMPClientException ("The X509 certificate did not contain a certificate subject.");
}
}
else
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Following SMP redirects is disabled");
}
return aMetadata;
}
/**
* Gets a signed service metadata object given by its service group id and its
* document type. This is a specification compliant method.
*
* @param aServiceGroupID
* The service group id of the service metadata to get. May not be
* null
.
* @param aDocumentTypeID
* The document type of the service metadata to get. May not be
* null
.
* @return A signed service metadata object or null
if no such
* registration is present.
* @throws SMPClientException
* in case something goes wrong
* @throws SMPClientUnauthorizedException
* A HTTP Forbidden was received, should not happen.
* @throws SMPClientBadRequestException
* The request was not well formed.
* @see #getServiceMetadata(IParticipantIdentifier, IDocumentTypeIdentifier)
* @since v8.0.0
*/
@Nullable
public SignedServiceMetadataType getServiceMetadataOrNull (@Nonnull final IParticipantIdentifier aServiceGroupID,
@Nonnull final IDocumentTypeIdentifier aDocumentTypeID) throws SMPClientException
{
try
{
return getServiceMetadata (aServiceGroupID, aDocumentTypeID);
}
catch (final SMPClientNotFoundException ex)
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Found no ServiceMetadata");
return null;
}
}
/**
* Extract the Endpoint from the signedServiceMetadata that matches the passed
* process ID and the optional required transport profile.
*
* @param aSignedServiceMetadata
* The signed service meta data object (e.g. from a call to
* {@link #getServiceMetadataOrNull(IParticipantIdentifier, IDocumentTypeIdentifier)}
* . May not be null
.
* @param aProcessID
* The process identifier to be looked up. May not be null
* .
* @param aTransportProfile
* The required transport profile to be used. May not be
* null
.
* @return null
if no matching endpoint was found
*/
@Nullable
public static EndpointType getEndpoint (@Nonnull final SignedServiceMetadataType aSignedServiceMetadata,
@Nonnull final IProcessIdentifier aProcessID,
@Nonnull final ISMPTransportProfile aTransportProfile)
{
ValueEnforcer.notNull (aSignedServiceMetadata, "SignedServiceMetadata");
return getEndpoint (aSignedServiceMetadata.getServiceMetadata (), aProcessID, aTransportProfile);
}
/**
* Extract the Endpoint from the ServiceMetadata that matches the passed
* process ID and the optional required transport profile.
*
* @param aServiceMetadata
* The unsigned service meta data object. May not be null
.
* @param aProcessID
* The process identifier to be looked up. May not be null
* .
* @param aTransportProfile
* The required transport profile to be used. May not be
* null
.
* @return null
if no matching endpoint was found
* @since 8.2.6
*/
@Nullable
public static EndpointType getEndpoint (@Nonnull final ServiceMetadataType aServiceMetadata,
@Nonnull final IProcessIdentifier aProcessID,
@Nonnull final ISMPTransportProfile aTransportProfile)
{
ValueEnforcer.notNull (aServiceMetadata, "ServiceMetadata");
final ServiceInformationType aServiceInformation = aServiceMetadata.getServiceInformation ();
if (aServiceInformation == null)
{
// It seems to be a redirect and not service information
return null;
}
ValueEnforcer.notNull (aServiceInformation.getProcessList (), "ServiceMetadata.ServiceInformation.ProcessList");
ValueEnforcer.notNull (aProcessID, "ProcessID");
ValueEnforcer.notNull (aTransportProfile, "TransportProfile");
// Iterate all processes
for (final ProcessType aProcessType : aServiceInformation.getProcessList ().getProcess ())
{
// Matches the requested one?
if (SimpleProcessIdentifier.wrap (aProcessType.getProcessIdentifier ()).hasSameContent (aProcessID))
{
// Filter endpoints by required transport profile
final ICommonsList aRelevantEndpoints = new CommonsArrayList <> ();
for (final EndpointType aEndpoint : aProcessType.getServiceEndpointList ().getEndpoint ())
if (aTransportProfile.getID ().equals (aEndpoint.getTransportProfile ()))
aRelevantEndpoints.add (aEndpoint);
if (aRelevantEndpoints.size () != 1)
{
LOGGER.warn ("Found " +
aRelevantEndpoints.size () +
" endpoints for process " +
aProcessID +
" and transport profile " +
aTransportProfile.getID () +
(aRelevantEndpoints.isEmpty () ? ""
: ": " +
aRelevantEndpoints.toString () +
" - using the first one"));
}
// Use the first endpoint or null
final EndpointType ret = aRelevantEndpoints.getFirstOrNull ();
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Found matching endpoint: " + ret);
return ret;
}
}
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Found no matching SMP endpoint");
return null;
}
/**
* Get the endpoint address URI from the provided SMP endpoint.
*
* @param aEndpoint
* The endpoint to be used. May be null
.
* @return null
if the endpoint is null
if the
* endpoint has no address URI.
*/
@Nullable
public static String getEndpointAddress (@Nullable final EndpointType aEndpoint)
{
return aEndpoint == null ? null : aEndpoint.getEndpointURI ();
}
/**
* Get the certificate bytes from the provided SMP endpoint.
*
* @param aEndpoint
* The endpoint to be used. May be null
.
* @return null
if the endpoint is null
if the
* endpoint has no certificate.
* @since 7.0.6
*/
@Nullable
public static byte [] getEndpointCertificateBytes (@Nullable final EndpointType aEndpoint)
{
return aEndpoint == null ? null : aEndpoint.getCertificate ();
}
/**
* Get the certificate bytes from the specified endpoint.
*
* @param aEndpoint
* The endpoint to be used. May be null
.
* @return null
if no such endpoint exists, or if the endpoint
* has no certificate
* @throws CertificateException
* In case the conversion from byte to X509 certificate failed
*/
@Nullable
public static X509Certificate getEndpointCertificate (@Nullable final EndpointType aEndpoint) throws CertificateException
{
final byte [] aCertBytes = getEndpointCertificateBytes (aEndpoint);
return CertificateHelper.convertByteArrayToCertficateDirect (aCertBytes);
}
/**
* Get the certificate bytes from the specified endpoint.
*
* @param aEndpoint
* The endpoint to be used. May be null
.
* @return null
if no such endpoint exists, or if the endpoint
* has no certificate or if the certificate is invalid.
* @since 8.1.2
*/
@Nullable
public static X509Certificate getEndpointCertificateOrNull (@Nullable final EndpointType aEndpoint)
{
try
{
return getEndpointCertificate (aEndpoint);
}
catch (final CertificateException ex)
{
return null;
}
}
/**
* Returns a service group. A service group references to the service
* metadata.
*
* @param aURLProvider
* The URL provider to be used. May not be null
.
* @param aSMLInfo
* The SML object to be used
* @param aServiceGroupID
* The service group id corresponding to the service group which one
* wants to get.
* @return The service group
* @throws SMPClientException
* in case something goes wrong
* @throws SMPClientUnauthorizedException
* A HTTP Forbidden was received, should not happen.
* @throws SMPClientNotFoundException
* The service group id did not exist.
* @throws SMPClientBadRequestException
* The request was not well formed.
* @throws SMPDNSResolutionException
* if DNS resolution fails
*/
@Nonnull
public static ServiceGroupType getServiceGroupByDNS (@Nonnull final ISMPURLProvider aURLProvider,
@Nonnull final ISMLInfo aSMLInfo,
@Nonnull final IParticipantIdentifier aServiceGroupID) throws SMPClientException,
SMPDNSResolutionException
{
return new BDXRClientReadOnly (aURLProvider, aServiceGroupID, aSMLInfo).getServiceGroup (aServiceGroupID);
}
/**
* Gets a signed service metadata object given by its service group id and its
* document type.
*
* @param aURLProvider
* The URL provider to be used. May not be null
.
* @param aSMLInfo
* The SML object to be used
* @param aServiceGroupID
* The service group id of the service metadata to get.
* @param aDocumentTypeID
* The document type of the service metadata to get.
* @return A signed service metadata object.
* @throws SMPClientException
* in case something goes wrong
* @throws SMPClientUnauthorizedException
* A HTTP Forbidden was received, should not happen.
* @throws SMPClientNotFoundException
* The service group id or document type did not exist.
* @throws SMPClientBadRequestException
* The request was not well formed.
* @throws SMPDNSResolutionException
* if DNS resolution fails
*/
@Nonnull
public static SignedServiceMetadataType getServiceRegistrationByDNS (@Nonnull final ISMPURLProvider aURLProvider,
@Nonnull final ISMLInfo aSMLInfo,
@Nonnull final IParticipantIdentifier aServiceGroupID,
@Nonnull final IDocumentTypeIdentifier aDocumentTypeID) throws SMPClientException,
SMPDNSResolutionException
{
return new BDXRClientReadOnly (aURLProvider, aServiceGroupID, aSMLInfo).getServiceMetadata (aServiceGroupID,
aDocumentTypeID);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy