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}