Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (C) 2024 Philip Helger (www.helger.com)
* 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.phase4.dbnalliance;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.time.OffsetDateTime;
import java.util.UUID;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import javax.annotation.concurrent.Immutable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.state.ESuccess;
import com.helger.peppol.smp.ESMPTransportProfile;
import com.helger.peppol.utils.PeppolCertificateHelper;
import com.helger.peppol.xhe.DBNAlliancePayload;
import com.helger.peppol.xhe.DBNAllianceXHEData;
import com.helger.peppol.xhe.write.DBNAllianceXHEDocumentWriter;
import com.helger.peppolid.IDocumentTypeIdentifier;
import com.helger.peppolid.IParticipantIdentifier;
import com.helger.peppolid.IProcessIdentifier;
import com.helger.peppolid.factory.BDXR2IdentifierFactory;
import com.helger.phase4.CAS4;
import com.helger.phase4.attachment.AS4OutgoingAttachment;
import com.helger.phase4.crypto.ICryptoSessionKeyProvider;
import com.helger.phase4.dynamicdiscovery.AS4EndpointDetailProviderBDXR2;
import com.helger.phase4.dynamicdiscovery.AS4EndpointDetailProviderConstant;
import com.helger.phase4.dynamicdiscovery.IAS4EndpointDetailProvider;
import com.helger.phase4.mgr.MetaAS4Manager;
import com.helger.phase4.profile.dbnalliance.DBNAlliancePMode;
import com.helger.phase4.sender.AbstractAS4UserMessageBuilderMIMEPayload;
import com.helger.phase4.sender.IAS4SendingDateTimeConsumer;
import com.helger.phase4.util.Phase4Exception;
import com.helger.smpclient.bdxr2.IBDXR2ServiceMetadataProvider;
import com.helger.xhe.v10.CXHE10;
import com.helger.xhe.v10.XHE10Marshaller;
import com.helger.xhe.v10.XHE10XHEType;
/**
* This class contains all the specifics to send AS4 messages with the
* DBNAlliance profile. See sendAS4Message as the main method to
* trigger the sending, with all potential customization.
*
* @author Robinson Artemio Garcia Meléndez
* @author Philip Helger
*/
@Immutable
public final class Phase4DBNAllianceSender
{
public static final BDXR2IdentifierFactory IF = BDXR2IdentifierFactory.INSTANCE;
private static final Logger LOGGER = LoggerFactory.getLogger (Phase4DBNAllianceSender.class);
private Phase4DBNAllianceSender ()
{}
@Nullable
private static XHE10XHEType _createXHE (@Nonnull final IParticipantIdentifier aSenderID,
@Nonnull final IParticipantIdentifier aReceiverID,
@Nonnull final IDocumentTypeIdentifier aDocTypeID,
@Nonnull final IProcessIdentifier aProcID,
@Nonnull final Element aPayloadElement,
final boolean bClonePayloadElement)
{
final DBNAllianceXHEData aData = new DBNAllianceXHEData (IF);
aData.setFromParty (aSenderID.getScheme (), aSenderID.getValue ());
aData.setToParty (aReceiverID.getScheme (), aReceiverID.getValue ());
aData.setID (UUID.randomUUID ().toString ());
aData.setCreationDateAndTime (MetaAS4Manager.getTimestampMgr ().getCurrentXMLDateTime ());
final DBNAlliancePayload aPayload = new DBNAlliancePayload (IF);
aPayload.setCustomizationID (null, aDocTypeID.getValue ());
aPayload.setProfileID (aProcID.getScheme (), aProcID.getValue ());
// Not cloning the payload element is for saving memory only (if it can be
// ensured, the source payload element is not altered externally of course)
if (bClonePayloadElement)
aPayload.setPayloadContent (aPayloadElement);
else
aPayload.setPayloadContentNoClone (aPayloadElement);
aData.addPayload (aPayload);
// check with logging
if (!aData.areAllFieldsSet (true))
throw new IllegalArgumentException ("The DBNAlliance XHE data is incomplete. See logs for details.");
return DBNAllianceXHEDocumentWriter.createExchangeHeaderEnvelope (aData);
}
/**
* @return Create a new Builder for AS4 messages if the XHE payload is
* present. Never null.
*/
@Nonnull
public static DBNAllianceUserMessageBuilder builder ()
{
return new DBNAllianceUserMessageBuilder ();
}
/**
* Abstract DBNAlliance UserMessage builder class with sanity methods.
*
* @author Robinson Artemio Garcia Meléndez
* @author Philip Helger
* @param
* The implementation type
*/
public abstract static class AbstractDBNAllianceUserMessageBuilder >
extends
AbstractAS4UserMessageBuilderMIMEPayload
{
// C1
protected IParticipantIdentifier m_aSenderID;
// C4
protected IParticipantIdentifier m_aReceiverID;
protected IDocumentTypeIdentifier m_aDocTypeID;
protected IProcessIdentifier m_aProcessID;
protected IAS4EndpointDetailProvider m_aEndpointDetailProvider;
private Consumer m_aCertificateConsumer;
private Consumer m_aAPEndpointURLConsumer;
// Status var
private OffsetDateTime m_aEffectiveSendingDT;
protected AbstractDBNAllianceUserMessageBuilder ()
{
// Override default values
try
{
httpClientFactory (new Phase4DBNAllianceHttpClientSettings ());
agreementRef (DBNAlliancePMode.DEFAULT_AGREEMENT_ID);
fromPartyIDType (DBNAlliancePMode.DEFAULT_PARTY_TYPE_ID);
fromRole (CAS4.DEFAULT_INITIATOR_URL);
toPartyIDType (DBNAlliancePMode.DEFAULT_PARTY_TYPE_ID);
toRole (CAS4.DEFAULT_RESPONDER_URL);
// Other crypt parameters are located in the PMode security part
cryptParams ().setSessionKeyProvider (ICryptoSessionKeyProvider.INSTANCE_RANDOM_AES_256);
}
catch (final Exception ex)
{
throw new IllegalStateException ("Failed to init AS4 Client builder", ex);
}
}
/**
* Set the sender participant ID of the message. The participant ID must be
* provided prior to sending.
*
* @param aSenderID
* The sender participant ID. May not be null.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE senderParticipantID (@Nonnull final IParticipantIdentifier aSenderID)
{
ValueEnforcer.notNull (aSenderID, "SenderID");
if (m_aSenderID != null)
LOGGER.warn ("An existing SenderParticipantID is overridden");
m_aSenderID = aSenderID;
return thisAsT ();
}
/**
* Set the receiver participant ID of the message. The participant ID must
* be provided prior to sending. This ends up in the "finalRecipient"
* UserMessage property.
*
* @param aReceiverID
* The receiver participant ID. May not be null.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE receiverParticipantID (@Nonnull final IParticipantIdentifier aReceiverID)
{
ValueEnforcer.notNull (aReceiverID, "ReceiverID");
if (m_aReceiverID != null)
LOGGER.warn ("An existing ReceiverParticipantID is overridden");
m_aReceiverID = aReceiverID;
return thisAsT ();
}
/**
* @return The currently set Document Type ID. May be null.
*/
@Nullable
public final IDocumentTypeIdentifier documentTypeID ()
{
return m_aDocTypeID;
}
/**
* Set the document type ID to be send. The document type must be provided
* prior to sending. This is a shortcut to the {@link #action(String)}
* method.
*
* @param aDocTypeID
* The document type ID to be used. May not be null.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE documentTypeID (@Nonnull final IDocumentTypeIdentifier aDocTypeID)
{
ValueEnforcer.notNull (aDocTypeID, "DocTypeID");
if (m_aDocTypeID != null)
LOGGER.warn ("An existing DocumentTypeID is overridden");
m_aDocTypeID = aDocTypeID;
return action (aDocTypeID.getURIEncoded ());
}
/**
* @return The currently set Process ID. May be null.
*/
@Nullable
public final IProcessIdentifier processID ()
{
return m_aProcessID;
}
/**
* Set the process ID to be send. The process ID must be provided prior to
* sending. This is a shortcut to the {@link #service(String, String)}
* method.
*
* @param aProcessID
* The process ID to be used. May not be null.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE processID (@Nonnull final IProcessIdentifier aProcessID)
{
ValueEnforcer.notNull (aProcessID, "ProcessID");
if (m_aProcessID != null)
LOGGER.warn ("An existing ProcessID is overridden");
m_aProcessID = aProcessID;
return service (aProcessID.getScheme (), aProcessID.getValue ());
}
/**
* Set the abstract endpoint detail provider to be used. This can be an SMP
* lookup routine or in certain test cases a predefined certificate and
* endpoint URL.
*
* @param aEndpointDetailProvider
* The endpoint detail provider to be used. May not be
* null.
* @return this for chaining
* @see #smpClient(IBDXR2ServiceMetadataProvider)
*/
@Nonnull
public final IMPLTYPE endpointDetailProvider (@Nonnull final IAS4EndpointDetailProvider aEndpointDetailProvider)
{
ValueEnforcer.notNull (aEndpointDetailProvider, "EndpointDetailProvider");
if (m_aEndpointDetailProvider != null)
LOGGER.warn ("An existing EndpointDetailProvider is overridden");
m_aEndpointDetailProvider = aEndpointDetailProvider;
return thisAsT ();
}
/**
* Set the SMP client to be used. This is the point where e.g. the
* differentiation between SMK and SML can be done. This must be set prior
* to sending. If the endpoint information are already known you can also
* use {@link #receiverEndpointDetails(X509Certificate, String)} instead.
*
* @param aSMPClient
* The SMP client to be used. May not be null.
* @return this for chaining
* @see #receiverEndpointDetails(X509Certificate, String)
* @see #endpointDetailProvider(IAS4EndpointDetailProvider)
*/
@Nonnull
public final IMPLTYPE smpClient (@Nonnull final IBDXR2ServiceMetadataProvider aSMPClient)
{
final AS4EndpointDetailProviderBDXR2 aEndpointDetailProvider = new AS4EndpointDetailProviderBDXR2 (aSMPClient);
aEndpointDetailProvider.setTransportProfile (ESMPTransportProfile.TRANSPORT_PROFILE_DBNA_AS4_v1);
return endpointDetailProvider (aEndpointDetailProvider);
}
/**
* Use this method to explicit set the AP certificate and AP endpoint URL
* that was retrieved externally (e.g. via an SMP call or for a static test
* case).
*
* @param aCert
* The Peppol AP certificate that should be used to encrypt the
* message for the receiver. May not be null.
* @param sDestURL
* The destination URL of the receiving AP to send the AS4 message
* to. Must be a valid URL and may neither be null nor
* empty.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE receiverEndpointDetails (@Nonnull final X509Certificate aCert,
@Nonnull @Nonempty final String sDestURL)
{
return endpointDetailProvider (new AS4EndpointDetailProviderConstant (aCert, sDestURL));
}
/**
* Set an optional Consumer for the retrieved certificate from the endpoint
* details provider, independent of its usability.
*
* @param aCertificateConsumer
* The consumer to be used. May be null.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE certificateConsumer (@Nullable final Consumer aCertificateConsumer)
{
m_aCertificateConsumer = aCertificateConsumer;
return thisAsT ();
}
/**
* Set an optional Consumer for the destination AP address retrieved from
* the endpoint details provider, independent of its usability.
*
* @param aAPEndpointURLConsumer
* The consumer to be used. May be null.
* @return this for chaining
*/
@Nonnull
public final IMPLTYPE endpointURLConsumer (@Nullable final Consumer aAPEndpointURLConsumer)
{
m_aAPEndpointURLConsumer = aAPEndpointURLConsumer;
return thisAsT ();
}
/**
* The effective sending date time of the message. That is set only if
* message sending takes place.
*
* @return The effective sending date time or null if the
* messages was not sent yet.
*/
@Nullable
public final OffsetDateTime effectiveSendingDateTime ()
{
return m_aEffectiveSendingDT;
}
protected final boolean isEndpointDetailProviderUsable ()
{
// Sender ID doesn't matter here
if (m_aReceiverID == null)
{
LOGGER.warn ("The field 'receiverID' is not set");
return false;
}
if (m_aDocTypeID == null)
{
LOGGER.warn ("The field 'docTypeID' is not set");
return false;
}
if (m_aProcessID == null)
{
LOGGER.warn ("The field 'processID' is not set");
return false;
}
if (m_aEndpointDetailProvider == null)
{
LOGGER.warn ("The field 'endpointDetailProvider' is not set");
return false;
}
return true;
}
@Override
@OverridingMethodsMustInvokeSuper
protected ESuccess finishFields () throws Phase4Exception
{
if (!isEndpointDetailProviderUsable ())
{
LOGGER.error ("At least one mandatory field for endpoint discovery is not set and therefore the AS4 message cannot be send.");
return ESuccess.FAILURE;
}
// e.g. SMP lookup. Throws an Phase4Exception in case of error
m_aEndpointDetailProvider.init (m_aDocTypeID, m_aProcessID, m_aReceiverID);
// Certificate from e.g. SMP lookup (may throw an exception)
final X509Certificate aReceiverCert = m_aEndpointDetailProvider.getReceiverAPCertificate ();
if (m_aCertificateConsumer != null)
{
m_aCertificateConsumer.accept (aReceiverCert);
}
receiverCertificate (aReceiverCert);
// URL from e.g. SMP lookup (may throw an exception)
final String sDestURL = m_aEndpointDetailProvider.getReceiverAPEndpointURL ();
if (m_aAPEndpointURLConsumer != null)
m_aAPEndpointURLConsumer.accept (sDestURL);
endpointURL (sDestURL);
// From receiver certificate
toPartyID (PeppolCertificateHelper.getSubjectCN (aReceiverCert));
// Super at the end
return super.finishFields ();
}
@Override
@OverridingMethodsMustInvokeSuper
public boolean isEveryRequiredFieldSet ()
{
if (!super.isEveryRequiredFieldSet ())
return false;
if (m_aPayload == null)
{
LOGGER.warn ("The field 'payload' is not set");
return false;
}
if (m_aReceiverID == null)
{
LOGGER.warn ("The field 'receiverID' is not set");
return false;
}
if (m_aDocTypeID == null)
{
LOGGER.warn ("The field 'docTypeID' is not set");
return false;
}
if (m_aProcessID == null)
{
LOGGER.warn ("The field 'processID' is not set");
return false;
}
if (m_aEndpointDetailProvider == null)
{
LOGGER.warn ("The field 'endpointDetailProvider' is not set");
return false;
}
// m_aCertificateConsumer may be null
// m_aAPEndpointURLConsumer may be null
// All valid
return true;
}
@Override
protected void customizeBeforeSending () throws Phase4Exception
{
// Explicitly remember the old handler
// Try to do this as close to sending as possible, to avoid that another
// sendingDateTimConsumer is used
final IAS4SendingDateTimeConsumer aExisting = m_aSendingDTConsumer;
sendingDateTimeConsumer (aSendingDT -> {
// Store in this instance
m_aEffectiveSendingDT = aSendingDT;
// Call the original handler
if (aExisting != null)
aExisting.onEffectiveSendingDateTime (aSendingDT);
});
}
}
/**
* The builder class for sending AS4 messages using DBNAlliance profile
* specifics. Use {@link #sendMessage()} to trigger the main transmission.
*
* @author Robinson Artemio Garcia Meléndez
* @author Philip Helger
*/
public static class DBNAllianceUserMessageBuilder extends
AbstractDBNAllianceUserMessageBuilder
{
private Element m_aPayloadElement;
public DBNAllianceUserMessageBuilder ()
{}
/**
* Set the payload element to be used, if it is available as a parsed DOM
* element. Internally the DOM element will be cloned before sending it out.
* If this method is called, it overwrites any other explicitly set payload.
*
* @param aPayloadElement
* The payload element to be used. They payload element MUST have a
* namespace URI. May not be null.
* @return this for chaining
*/
@Nonnull
public DBNAllianceUserMessageBuilder payload (@Nonnull final Element aPayloadElement)
{
ValueEnforcer.notNull (aPayloadElement, "Payload");
ValueEnforcer.notEmpty (aPayloadElement.getNamespaceURI (), "Payload.NamespaceURI");
m_aPayloadElement = aPayloadElement;
return this;
}
@Override
@OverridingMethodsMustInvokeSuper
protected ESuccess finishFields () throws Phase4Exception
{
// Ensure a DOM element is present
final Element aPayloadElement;
final boolean bClonePayloadElement;
if (m_aPayloadElement != null)
{
// Already provided as a DOM element
aPayloadElement = m_aPayloadElement;
bClonePayloadElement = true;
}
else
throw new IllegalStateException ("Unexpected - element is not present");
// Consistency check
if (CXHE10.NAMESPACE_URI_XHE.equals (aPayloadElement.getNamespaceURI ()))
throw new Phase4DBNAllianceException ("You cannot set a Exchange Header Envelope as the payload for the regular builder. The XHE is created automatically inside of this builder.");
// Optional payload validation
// _validatePayload (aPayloadElement, m_aVESRegistry, m_aVESID,
// m_aValidationResultHandler);
// Perform SMP lookup
if (super.finishFields ().isFailure ())
return ESuccess.FAILURE;
// Created SBDH
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Start creating SBDH for AS4 message");
final XHE10XHEType aXHE = _createXHE (m_aSenderID,
m_aReceiverID,
m_aDocTypeID,
m_aProcessID,
aPayloadElement,
bClonePayloadElement);
if (aXHE == null)
{
// A log message was already provided
return ESuccess.FAILURE;
}
final byte [] aXHEBytes = new XHE10Marshaller ().getAsBytes (aXHE);
// Now we have the main payload
payload (AS4OutgoingAttachment.builder ()
.data (aXHEBytes)
.compressionGZIP ()
.mimeTypeXML ()
.charset (StandardCharsets.UTF_8));
return ESuccess.SUCCESS;
}
}
}