Search code examples
javawsdlcxfunmarshallingonvif

Onvif - parsing event notification from WS-BaseNotification


I am currently implementing an event handler for Onvif. But I am totally lost with the unmarshalling process of the WS-BaseNotification.

  1. How can I understand / parse the NotificationMessageHolderType to the onvif event implementation ?

  2. I suppose the ws-basenotification have a method to map the "generic" message with a custom implementation. Where is the link / information to the schema to use ?

My test code : endpoint subscriber

private W3CEndpointReference subscribe(final FilterType filterType) {
    try {
        CreatePullPointSubscription parameters = new CreatePullPointSubscription();
        parameters.setFilter(filterType);
        CreatePullPointSubscriptionResponse pullPointSubscription = onvifDevice.getEventPort().createPullPointSubscription(parameters);
        return pullPointSubscription.getSubscriptionReference();

    } catch (TopicNotSupportedFault | InvalidMessageContentExpressionFault | InvalidProducerPropertiesExpressionFault | InvalidTopicExpressionFault | TopicExpressionDialectUnknownFault | UnacceptableInitialTerminationTimeFault | NotifyMessageNotSupportedFault | ResourceUnknownFault | UnsupportedPolicyRequestFault | InvalidFilterFault | SubscribeCreationFailedFault | UnrecognizedPolicyRequestFault topicNotSupportedFault) {
        topicNotSupportedFault.printStackTrace();
        return null;
    }
}

And I can receive the notifications and unmarshall them to a org.onvif.ver10.schema.Message.class :

Receive notification

PullMessagesResponse pullMessagesResponse = pullPointSubscription.pullMessages(pullMessages);
List<NotificationMessageHolderType> notificationMessage = pullMessagesResponse.getNotificationMessage();

// Some test to have understandable data.
TopicExpressionType topic = notificationMessage.get(0).getTopic();
JAXBContext context = JAXBContext.newInstance(org.onvif.ver10.schema.Message.class, AccessPointState.class);
Unmarshaller um = context.createUnmarshaller();
Object response = (Object)um.unmarshal((Node)node);

Example

PullMessagesResponse sent by the device (wireshark capture) :

<tev:PullMessagesResponse>
    <tev:CurrentTime>2020-04-29T11:41:52Z</tev:CurrentTime>
    <tev:TerminationTime>2020-04-29T11:46:51Z</tev:TerminationTime>
    <wsnt:NotificationMessage>
        <wsnt:Topic Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Simple">tns1:AccessPoint/State/Enabled</wsnt:Topic>
        <wsnt:ProducerReference>
            <wsa5:Address>uri://5581ad80-95b0-11e0-b883-accc8e251272/ProducerReference</wsa5:Address>
        </wsnt:ProducerReference>
        <wsnt:Message>
            <tt:Message UtcTime="2020-04-23T15:21:26.924984Z" PropertyOperation="Initialized">
                <tt:Source>
                    <tt:SimpleItem Name="AccessPointToken" Value="Axis-accc8e251272:1584710973.159018000"></tt:SimpleItem>
                    <tt:SimpleItem Name="Device Source" Value="5581ad80-95b0-11e0-b883-accc8e251272"></tt:SimpleItem>
                </tt:Source>
                <tt:Key></tt:Key>
                <tt:Data>
                    <tt:SimpleItem Name="State" Value="1"></tt:SimpleItem>
                </tt:Data>
            </tt:Message>
        </wsnt:Message>
    </wsnt:NotificationMessage>
</tev:PullMessagesResponse>

Then the unmarshalling result to a onvif.message :

enter image description here

Annexe

The Onvif-core-specification manual says (p106):

A notification answers the following questions: 
• When did it happen?
• Who produced the event? 
• What happened? 

 ... text ommited 

The “what” question is answered in two steps. First, the Topic element of the NotificationMessage is used to categorize the Event. Second, items are added to the Data element of the Message container in order to describe the details of the Event.

So I suppose the topics of my previous example "tns1:AccessPoint/State/Enabled" must be mapped to a type ? But I cannot found the description of this structure, and secondly I was hoping this mapping to be done automatically during the generation from the wsdl files.

The profile C - onvif documentation describe all the existings topics for my device, but there is no description of the content of this notifications...

Updated Apparently the request : GetEventPropertiesResponse return the attributes present in each topics. So I am looking to found a way to generated the "object" from this description.

enter image description here

Thanks for helping


Solution

  • I have found one working (if not elegant) solution

    Use with caution this is an implementation of 3 years old Onvif WSDL files.

    Fetching messages :

    private List<NotificationMessageHolderType> fetchNotifications(final PullPointSubscription pullPointSubscription) {
        try {
            return pullPointSubscription.pullMessages(pullMessages).getNotificationMessage();
        } catch (PullMessagesFaultResponse_Exception e) {
            // TODO your error handling...
            return Lists.newArrayList();
        }
    }
    

    Then I use this EventParser :

    import ch.swissdotnet.onvif.events.entities.OnvifEvent;
    import org.oasis_open.docs.wsn.b_2.NotificationMessageHolderType;
    import org.onvif.ver10.schema.Message;
    import org.reflections.Reflections;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.w3c.dom.Node;
    
    import javax.xml.bind.JAXBContext;
    import javax.xml.bind.JAXBException;
    import javax.xml.bind.Unmarshaller;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    import java.util.Optional;
    import java.util.Set;
    
    public class EventParser {
    
        public static final Logger LOG = LoggerFactory.getLogger(EventParser.class);
    
        private final Unmarshaller um2;
        private final Set<Class<? extends OnvifEvent>> events;
    
        public EventParser(final String eventPackage) throws JAXBException {
            Reflections reflections = new Reflections(eventPackage);
            this.events = reflections.getSubTypesOf(OnvifEvent.class);
            this.um2 = JAXBContext.newInstance(Message.class).createUnmarshaller();
        }
    
        public Optional<? extends OnvifEvent> parse(final NotificationMessageHolderType notificationMessageHolderType) {
            try {
                String topic = ((String) notificationMessageHolderType.getTopic().getContent().get(0));
                Message message = unmarshallToMessage(notificationMessageHolderType);
                return getEvent(message, topic);
    
            } catch (Exception e) {
                LOG.error("Receive not understandable event case [{}] message [{}]", e.getCause(), e.getMessage(), e);
                return Optional.empty();
            }
        }
    
        private Optional<? extends OnvifEvent> getEvent(final Message message, final String topic) throws Exception {
            for (Class<? extends OnvifEvent> event : events) {
    
                EventTopic annotatedTopic = event.getDeclaredAnnotation(EventTopic.class);
                if (topic.equals(annotatedTopic.topic())) {
                    try {
                        Constructor<? extends OnvifEvent> constructor = event.getConstructor(Message.class);
                        return Optional.of(constructor.newInstance(message));
                    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                        throw new Exception("Invalid Onvif event", e);
                    }
                }
            }
    
            return Optional.empty();
        }
    
        private Message unmarshallToMessage(NotificationMessageHolderType notification) throws JAXBException {
            Node node = (Node) notification.getMessage().getAny();
            return (org.onvif.ver10.schema.Message) um2.unmarshal(node);
        }
    }
    

    The ugly come now with checking for every kind of event with instance of (this is just a demo with DoorAlarm).

    public void onEvent(final OnvifEvent event) {
        if (event instanceof DoorAlarm) {
            var alarmEvent = (DoorAlarm) event;
            List<DoorEventCallback> listeners = mapListenersByDoor(alarmEvent.getSourceDoorAlarm().getDoorToken());
            listeners.forEach(listener -> listener.onAlarm(alarmEvent));
            return;
        }
    

    ...