001/**
002 *
003 * Copyright 2018-2019 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack;
018
019import java.io.IOException;
020import java.util.HashMap;
021import java.util.Map;
022
023import javax.xml.namespace.QName;
024
025import org.jivesoftware.smack.SmackException.NoResponseException;
026import org.jivesoftware.smack.SmackException.NotConnectedException;
027import org.jivesoftware.smack.XMPPException.FailedNonzaException;
028import org.jivesoftware.smack.packet.Nonza;
029import org.jivesoftware.smack.util.XmppElementUtil;
030
031public class NonzaCallback {
032
033    protected final AbstractXMPPConnection connection;
034    protected final Map<QName, ClassAndConsumer<? extends Nonza>> filterAndListeners;
035
036    private NonzaCallback(Builder builder) {
037        this.connection = builder.connection;
038        this.filterAndListeners = builder.filterAndListeners;
039        install();
040    }
041
042    void onNonzaReceived(Nonza nonza) throws IOException {
043        QName key = nonza.getQName();
044        ClassAndConsumer<? extends Nonza> classAndConsumer = filterAndListeners.get(key);
045
046        classAndConsumer.accept(nonza);
047    }
048
049    public void cancel() {
050        for (Map.Entry<QName, ClassAndConsumer<? extends Nonza>> entry : filterAndListeners.entrySet()) {
051            QName filterKey = entry.getKey();
052            synchronized (connection.nonzaCallbacksMap) {
053                connection.nonzaCallbacksMap.removeOne(filterKey, this);
054            }
055        }
056    }
057
058    protected void install() {
059        if (filterAndListeners.isEmpty()) {
060            return;
061        }
062
063        for (QName key : filterAndListeners.keySet()) {
064            synchronized (connection.nonzaCallbacksMap) {
065                connection.nonzaCallbacksMap.put(key, this);
066            }
067        }
068    }
069
070    private static final class NonzaResponseCallback<SN extends Nonza, FN extends Nonza> extends NonzaCallback {
071
072        private SN successNonza;
073        private FN failedNonza;
074
075        private NonzaResponseCallback(Class<SN> successNonzaClass, Class<FN> failedNonzaClass,
076                        Builder builder) {
077            super(builder);
078
079            final QName successNonzaKey = XmppElementUtil.getQNameFor(successNonzaClass);
080            final QName failedNonzaKey = XmppElementUtil.getQNameFor(failedNonzaClass);
081
082            final NonzaListener<SN> successListener = new NonzaListener<SN>() {
083                @Override
084                public void accept(SN successNonza) {
085                    NonzaResponseCallback.this.successNonza = successNonza;
086                    notifyResponse();
087                }
088            };
089            final ClassAndConsumer<SN> successClassAndConsumer = new ClassAndConsumer<>(successNonzaClass,
090                            successListener);
091
092            final NonzaListener<FN> failedListener = new NonzaListener<FN>() {
093                @Override
094                public void accept(FN failedNonza) {
095                    NonzaResponseCallback.this.failedNonza = failedNonza;
096                    notifyResponse();
097                }
098            };
099            final ClassAndConsumer<FN> failedClassAndConsumer = new ClassAndConsumer<>(failedNonzaClass,
100                            failedListener);
101
102            filterAndListeners.put(successNonzaKey, successClassAndConsumer);
103            filterAndListeners.put(failedNonzaKey, failedClassAndConsumer);
104
105            install();
106        }
107
108        private void notifyResponse() {
109            synchronized (this) {
110                notifyAll();
111            }
112        }
113
114        private boolean hasReceivedSuccessOrFailedNonza() {
115            return successNonza != null || failedNonza != null;
116        }
117
118        private SN waitForResponse() throws NoResponseException, InterruptedException, FailedNonzaException {
119            final long deadline = System.currentTimeMillis() + connection.getReplyTimeout();
120            synchronized (this) {
121                while (!hasReceivedSuccessOrFailedNonza()) {
122                    final long now = System.currentTimeMillis();
123                    if (now >= deadline) break;
124                    wait(deadline - now);
125                }
126            }
127
128            if (!hasReceivedSuccessOrFailedNonza()) {
129                throw NoResponseException.newWith(connection, "Nonza Listener");
130            }
131
132            if (failedNonza != null) {
133                throw new XMPPException.FailedNonzaException(failedNonza);
134            }
135
136            assert successNonza != null;
137            return successNonza;
138        }
139    }
140
141    public static final class Builder {
142        private final AbstractXMPPConnection connection;
143
144        private Map<QName, ClassAndConsumer<? extends Nonza>> filterAndListeners = new HashMap<>();
145
146        Builder(AbstractXMPPConnection connection) {
147            this.connection = connection;
148        }
149
150        public <N extends Nonza> Builder listenFor(Class<N> nonza, NonzaListener<N> nonzaListener) {
151            QName key = XmppElementUtil.getQNameFor(nonza);
152            ClassAndConsumer<N> classAndConsumer = new ClassAndConsumer<>(nonza, nonzaListener);
153            filterAndListeners.put(key, classAndConsumer);
154            return this;
155        }
156
157        public NonzaCallback install() {
158            return new NonzaCallback(this);
159        }
160    }
161
162    public interface NonzaListener<N extends Nonza> {
163        void accept(N nonza) throws IOException;
164    }
165
166    private static final class ClassAndConsumer<N extends Nonza> {
167        private final Class<N> clazz;
168        private final NonzaListener<N> consumer;
169
170        private ClassAndConsumer(Class<N> clazz, NonzaListener<N> consumer) {
171            this.clazz = clazz;
172            this.consumer = consumer;
173        }
174
175        private void accept(Object object) throws IOException {
176            N nonza = clazz.cast(object);
177            consumer.accept(nonza);
178        }
179    }
180
181    static <SN extends Nonza, FN extends Nonza> SN sendAndWaitForResponse(NonzaCallback.Builder builder, Nonza nonza, Class<SN> successNonzaClass,
182                    Class<FN> failedNonzaClass)
183                    throws NoResponseException, NotConnectedException, InterruptedException, FailedNonzaException {
184        NonzaResponseCallback<SN, FN> nonzaCallback = new NonzaResponseCallback<>(successNonzaClass,
185                        failedNonzaClass, builder);
186
187        SN successNonza;
188        try {
189            nonzaCallback.connection.sendNonza(nonza);
190            successNonza = nonzaCallback.waitForResponse();
191        }
192        finally {
193            nonzaCallback.cancel();
194        }
195
196        return successNonza;
197    }
198}