diff options
author | Daniel Gultsch <daniel.gultsch@rwth-aachen.de> | 2014-04-17 14:52:10 +0200 |
---|---|---|
committer | Andreas Straub <andy@strb.org> | 2014-04-18 00:17:34 +0200 |
commit | 07cf07ad58b9e99d9b63da1e00529c4e3bda721f (patch) | |
tree | a1dc28f88ce193f9d6e7ae84dac344d64570d0b2 | |
parent | 8006931f80fcba014c141bc726307ceea782f4fe (diff) |
lot of cleanup in jingle part
6 files changed, 326 insertions, 120 deletions
diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java new file mode 100644 index 000000000..80ffeaaac --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -0,0 +1,138 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.xml.Element; + +public class JingleCandidate { + + public static int TYPE_UNKNOWN; + public static int TYPE_DIRECT = 0; + public static int TYPE_PROXY = 1; + + private boolean ours; + private boolean usedByCounterpart = false; + private String cid; + private String host; + private int port; + private int type; + private String jid; + private int priority; + + public JingleCandidate(String cid,boolean ours) { + this.ours = ours; + this.cid = cid; + } + + public String getCid() { + return cid; + } + + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return this.host; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + + public void setType(int type) { + this.type = type; + } + + public void setType(String type) { + if ("proxy".equals(type)) { + this.type = TYPE_PROXY; + } else if ("direct".equals(type)) { + this.type = TYPE_DIRECT; + } else { + this.type = TYPE_UNKNOWN; + } + } + + public void setPriority(int i) { + this.priority = i; + } + + public int getPriority() { + return this.priority; + } + + public boolean equals(JingleCandidate other) { + return this.getCid().equals(other.getCid()); + } + + public boolean equalValues(JingleCandidate other) { + return other.getHost().equals(this.getHost())&&(other.getPort()==this.getPort()); + } + + public boolean isOurs() { + return ours; + } + + public int getType() { + return this.type; + } + + public static List<JingleCandidate> parse(List<Element> canditates) { + List<JingleCandidate> parsedCandidates = new ArrayList<JingleCandidate>(); + for(Element c : canditates) { + parsedCandidates.add(JingleCandidate.parse(c)); + } + return parsedCandidates; + } + + public static JingleCandidate parse(Element candidate) { + JingleCandidate parsedCandidate = new JingleCandidate(candidate.getAttribute("cid"), false); + parsedCandidate.setHost(candidate.getAttribute("host")); + parsedCandidate.setJid(candidate.getAttribute("jid")); + parsedCandidate.setType(candidate.getAttribute("type")); + parsedCandidate.setPriority(Integer.parseInt(candidate.getAttribute("priority"))); + parsedCandidate.setPort(Integer.parseInt(candidate.getAttribute("port"))); + return parsedCandidate; + } + + public Element toElement() { + Element element = new Element("candidate"); + element.setAttribute("cid", this.getCid()); + element.setAttribute("host", this.getHost()); + element.setAttribute("port", ""+this.getPort()); + element.setAttribute("jid", this.getJid()); + element.setAttribute("priority",""+this.getPriority()); + if (this.getType()==TYPE_DIRECT) { + element.setAttribute("type","direct"); + } else if (this.getType()==TYPE_PROXY) { + element.setAttribute("type","proxy"); + } + return element; + } + + public void flagAsUsedByCounterpart() { + this.usedByCounterpart = true; + } + + public boolean isUsedByCounterpart() { + return this.usedByCounterpart; + } + + public String toString() { + return this.getHost()+":"+this.getPort()+" (prio="+this.getPriority()+")"; + } +} diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 75c2c8e07..a7fd367d0 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -38,12 +38,15 @@ public class JingleConnection { private Account account; private String initiator; private String responder; - private List<Element> candidates = new ArrayList<Element>(); - private List<String> candidatesUsedByCounterpart = new ArrayList<String>(); + private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>(); private HashMap<String, SocksConnection> connections = new HashMap<String, SocksConnection>(); - private Content content = new Content(); + + private String transportId; + private Element fileOffer; private JingleFile file = null; + private boolean receivedCandidateError = false; + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { @Override @@ -107,10 +110,11 @@ public class JingleConnection { this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() { @Override - public void onPrimaryCandidateFound(boolean success, Element candidate) { + public void onPrimaryCandidateFound(boolean success, JingleCandidate candidate) { if (success) { mergeCandidate(candidate); } + openOurCandidates(); sendInitRequest(); } }); @@ -130,9 +134,10 @@ public class JingleConnection { this.initiator = packet.getFrom(); this.responder = this.account.getFullJid(); this.sessionId = packet.getSessionId(); - this.content = packet.getJingleContent(); - this.mergeCandidates(this.content.getCanditates()); - Element fileOffer = packet.getJingleContent().getFileOffer(); + Content content = packet.getJingleContent(); + this.transportId = content.getTransportId(); + this.mergeCandidates(JingleCandidate.parse(content.getCanditates())); + this.fileOffer = packet.getJingleContent().getFileOffer(); if (fileOffer!=null) { this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message); Element fileSize = fileOffer.findChild("size"); @@ -156,13 +161,14 @@ public class JingleConnection { private void sendInitRequest() { JinglePacket packet = this.bootstrapPacket("session-initiate"); - this.content = new Content(); + Content content = new Content(); if (message.getType() == Message.TYPE_IMAGE) { content.setAttribute("creator", "initiator"); content.setAttribute("name", "a-file-offer"); this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message); content.setFileOffer(this.file); - content.setCandidates(this.mJingleConnectionManager.nextRandomId(),this.candidates); + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setCandidates(this.transportId,getCandidatesAsElements()); packet.setContent(content); Log.d("xmppService",packet.toString()); account.getXmppConnection().sendIqPacket(packet, this.responseListener); @@ -170,17 +176,28 @@ public class JingleConnection { } } + private List<Element> getCandidatesAsElements() { + List<Element> elements = new ArrayList<Element>(); + for(JingleCandidate c : this.candidates) { + elements.add(c.toElement()); + } + return elements; + } + private void sendAccept() { this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() { @Override - public void onPrimaryCandidateFound(boolean success, Element candidate) { + public void onPrimaryCandidateFound(boolean success, JingleCandidate candidate) { + Content content = new Content(); + content.setFileOffer(fileOffer); if (success) { if (!equalCandidateExists(candidate)) { mergeCandidate(candidate); - content.addCandidate(candidate); } } + openOurCandidates(); + content.setCandidates(transportId, getCandidatesAsElements()); JinglePacket packet = bootstrapPacket("session-accept"); packet.setContent(content); account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() { @@ -211,7 +228,7 @@ public class JingleConnection { private void accept(JinglePacket packet) { Log.d("xmppService","session-accept: "+packet.toString()); Content content = packet.getJingleContent(); - mergeCandidates(content.getCanditates()); + mergeCandidates(JingleCandidate.parse(content.getCanditates())); this.status = STATUS_ACCEPTED; this.connectNextCandidate(); IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); @@ -224,26 +241,25 @@ public class JingleConnection { IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); if (cid!=null) { Log.d("xmppService","candidate used by counterpart:"+cid); - this.candidatesUsedByCounterpart.add(cid); - if (this.connections.containsKey(cid)) { - SocksConnection connection = this.connections.get(cid); - if (connection.isEstablished()) { - if (status==STATUS_ACCEPTED) { - this.connect(connection); - } else { - Log.d("xmppService","ignoring canditate used because we are already transmitting"); - } - } else { - Log.d("xmppService","not yet connected. check when callback comes back"); - } + JingleCandidate candidate = getCandidate(cid); + candidate.flagAsUsedByCounterpart(); + if (status == STATUS_ACCEPTED) { + this.connect(); } else { - Log.d("xmppService","candidate not yet in list of connections"); + Log.d("xmppService","ignoring because file is already in transmission"); + } + } else if (content.hasCandidateError()) { + Log.d("xmppService","received candidate error"); + this.receivedCandidateError = true; + if (status == STATUS_ACCEPTED) { + this.connect(); } } account.getXmppConnection().sendIqPacket(response, null); } - private void connect(final SocksConnection connection) { + private void connect() { + final SocksConnection connection = chooseConnection(); this.status = STATUS_TRANSMITTING; final OnFileTransmitted callback = new OnFileTransmitted() { @@ -256,10 +272,10 @@ public class JingleConnection { Log.d("xmppService","sucessfully transmitted file. sha1:"+file.getSha1Sum()); } }; - if ((connection.isProxy()&&(connection.getCid().equals(mJingleConnectionManager.getPrimaryCandidateId(account))))) { - Log.d("xmppService","candidate "+connection.getCid()+" was our proxy and needs activation"); + if (connection.isProxy()&&(connection.getCandidate().isOurs())) { + Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was our proxy and needs activation"); IqPacket activation = new IqPacket(IqPacket.TYPE_SET); - activation.setTo(connection.getJid()); + activation.setTo(connection.getCandidate().getJid()); activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId()); activation.query().addChild("activate").setContent(this.getCounterPart()); this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() { @@ -287,6 +303,41 @@ public class JingleConnection { } } + private SocksConnection chooseConnection() { + Log.d("xmppService","choosing connection from "+this.connections.size()+" possibilties"); + SocksConnection connection = null; + Iterator<Entry<String, SocksConnection>> it = this.connections.entrySet().iterator(); + while (it.hasNext()) { + Entry<String, SocksConnection> pairs = it.next(); + SocksConnection currentConnection = pairs.getValue(); + Log.d("xmppService","comparing candidate: "+currentConnection.getCandidate().toString()); + if (currentConnection.isEstablished()&&(currentConnection.getCandidate().isUsedByCounterpart()||(!currentConnection.getCandidate().isOurs()))) { + Log.d("xmppService","is usable"); + if (connection==null) { + connection = currentConnection; + } else { + if (connection.getCandidate().getPriority()<currentConnection.getCandidate().getPriority()) { + connection = currentConnection; + } else if (connection.getCandidate().getPriority()==currentConnection.getCandidate().getPriority()) { + Log.d("xmppService","found two candidates with same priority"); + if (initiator.equals(account.getFullJid())) { + if (currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } else { + if (!currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } + } + } + } + it.remove(); + } + Log.d("xmppService","chose candidate: "+connection.getCandidate().getHost()); + return connection; + } + private void sendSuccess() { JinglePacket packet = bootstrapPacket("session-terminate"); Reason reason = new Reason(); @@ -311,20 +362,40 @@ public class JingleConnection { this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED); } + private void openOurCandidates() { + for(JingleCandidate candidate : this.candidates) { + if (candidate.isOurs()) { + final SocksConnection socksConnection = new SocksConnection(this,candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnSocksConnection() { + + @Override + public void failed() { + Log.d("xmppService","connection to our candidate failed"); + } + + @Override + public void established() { + Log.d("xmppService","connection to our candidate was successful"); + } + }); + } + } + } + private void connectNextCandidate() { - for(Element candidate : this.candidates) { - String cid = candidate.getAttribute("cid"); - if (!connections.containsKey(cid)) { + for(JingleCandidate candidate : this.candidates) { + if ((!connections.containsKey(candidate.getCid())&&(!candidate.isOurs()))) { this.connectWithCandidate(candidate); - break; + return; } } + this.sendCandidateError(); } - private void connectWithCandidate(Element candidate) { - boolean initating = candidate.getAttribute("cid").equals(mJingleConnectionManager.getPrimaryCandidateId(account)); - final SocksConnection socksConnection = new SocksConnection(this,candidate,initating); - connections.put(socksConnection.getCid(), socksConnection); + private void connectWithCandidate(final JingleCandidate candidate) { + final SocksConnection socksConnection = new SocksConnection(this,candidate); + connections.put(candidate.getCid(), socksConnection); socksConnection.connect(new OnSocksConnection() { @Override @@ -334,14 +405,10 @@ public class JingleConnection { @Override public void established() { - if (candidatesUsedByCounterpart.contains(socksConnection.getCid())) { - if (status==STATUS_ACCEPTED) { - connect(socksConnection); - } else { - Log.d("xmppService","ignoring cuz already transmitting"); - } - } else { - sendCandidateUsed(socksConnection.getCid()); + sendCandidateUsed(candidate.getCid()); + if ((receivedCandidateError)&&(status == STATUS_ACCEPTED)) { + Log.d("xmppService","received candidate error before. trying to connect"); + connect(); } } }); @@ -359,21 +426,25 @@ public class JingleConnection { private void sendCandidateUsed(final String cid) { JinglePacket packet = bootstrapPacket("transport-info"); Content content = new Content(); - content.setUsedCandidate(this.content.getTransportId(), cid); + //TODO: put these into actual variables + content.setAttribute("creator", "initiator"); + content.setAttribute("name", "a-file-offer"); + content.setUsedCandidate(this.transportId, cid); packet.setContent(content); Log.d("xmppService","send using candidate: "+cid); - this.account.getXmppConnection().sendIqPacket(packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d("xmppService","got ack for our candidate used"); - if (status==STATUS_ACCEPTED) { - connect(connections.get(cid)); - } else { - Log.d("xmppService","ignoring cuz already transmitting"); - } - } - }); + this.account.getXmppConnection().sendIqPacket(packet,responseListener); + } + + private void sendCandidateError() { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(); + //TODO: put these into actual variables + content.setAttribute("creator", "initiator"); + content.setAttribute("name", "a-file-offer"); + content.setCandidateError(this.transportId); + packet.setContent(content); + Log.d("xmppService","send candidate error"); + this.account.getXmppConnection().sendIqPacket(packet,responseListener); } public String getInitiator() { @@ -388,33 +459,33 @@ public class JingleConnection { return this.status; } - private boolean equalCandidateExists(Element candidate) { - for(Element c : this.candidates) { - if (c.getAttribute("host").equals(candidate.getAttribute("host"))&&(c.getAttribute("port").equals(candidate.getAttribute("port")))) { + private boolean equalCandidateExists(JingleCandidate candidate) { + for(JingleCandidate c : this.candidates) { + if (c.equalValues(candidate)) { return true; } } return false; } - private void mergeCandidate(Element candidate) { - for(Element c : this.candidates) { - if (c.getAttribute("cid").equals(candidate.getAttribute("cid"))) { + private void mergeCandidate(JingleCandidate candidate) { + for(JingleCandidate c : this.candidates) { + if (c.equals(candidate)) { return; } } this.candidates.add(candidate); } - private void mergeCandidates(List<Element> candidates) { - for(Element c : candidates) { + private void mergeCandidates(List<JingleCandidate> candidates) { + for(JingleCandidate c : candidates) { mergeCandidate(c); } } - private Element getCandidate(String cid) { - for(Element c : this.candidates) { - if (c.getAttribute("cid").equals(cid)) { + private JingleCandidate getCandidate(String cid) { + for(JingleCandidate c : this.candidates) { + if (c.getCid().equals(cid)) { return c; } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 924674a7a..44af51a34 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -23,7 +23,7 @@ public class JingleConnectionManager { private List<JingleConnection> connections = new ArrayList<JingleConnection>(); // make // concurrent - private ConcurrentHashMap<String, Element> primaryCandidates = new ConcurrentHashMap<String, Element>(); + private ConcurrentHashMap<String, JingleCandidate> primaryCandidates = new ConcurrentHashMap<String, JingleCandidate>(); private SecureRandom random = new SecureRandom(); @@ -89,17 +89,12 @@ public class JingleConnectionManager { if (streamhost != null) { Log.d("xmppService", "streamhost found " + streamhost.toString()); - Element candidate = new Element("candidate"); - candidate.setAttribute("cid", - nextRandomId()); - candidate.setAttribute("host", - streamhost.getAttribute("host")); - candidate.setAttribute("port", - streamhost.getAttribute("port")); - candidate.setAttribute("type", "proxy"); - candidate.setAttribute("jid", proxy); - candidate - .setAttribute("priority", "655360"); + JingleCandidate candidate = new JingleCandidate(nextRandomId(),true); + candidate.setHost(streamhost.getAttribute("host")); + candidate.setPort(Integer.parseInt(streamhost.getAttribute("port"))); + candidate.setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(proxy); + candidate.setPriority(655360+65535); primaryCandidates.put(account.getJid(), candidate); listener.onPrimaryCandidateFound(true, @@ -119,14 +114,6 @@ public class JingleConnectionManager { this.primaryCandidates.get(account.getJid())); } } - - public String getPrimaryCandidateId(Account account) { - if (this.primaryCandidates.containsKey(account.getJid())) { - return this.primaryCandidates.get(account.getJid()).getAttribute("cid"); - } else { - return null; - } - } public String nextRandomId() { return new BigInteger(50, random).toString(32); diff --git a/src/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java index 9e00954ff..b91a90ff5 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java +++ b/src/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import eu.siacs.conversations.xml.Element; - public interface OnPrimaryCandidateFound { - public void onPrimaryCandidateFound(boolean success, Element canditate); + public void onPrimaryCandidateFound(boolean success, JingleCandidate canditate); } diff --git a/src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java b/src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java index affbb5f3c..197b9424c 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java +++ b/src/eu/siacs/conversations/xmpp/jingle/SocksConnection.java @@ -19,29 +19,20 @@ import android.util.Log; import android.widget.Button; public class SocksConnection { - private Socket socket; - private String host; - private String jid; - private String cid; - private int port; - private boolean isProxy = false; + private JingleCandidate candidate; private String destination; private OutputStream outputStream; private InputStream inputStream; private boolean isEstablished = false; + protected Socket socket; - public SocksConnection(JingleConnection jingleConnection, Element candidate, boolean initating) { - this.cid = candidate.getAttribute("cid"); - this.host = candidate.getAttribute("host"); - this.port = Integer.parseInt(candidate.getAttribute("port")); - String type = candidate.getAttribute("type"); - this.jid = candidate.getAttribute("jid"); - this.isProxy = "proxy".equalsIgnoreCase(type); + public SocksConnection(JingleConnection jingleConnection, JingleCandidate candidate) { + this.candidate = candidate; try { MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); StringBuilder destBuilder = new StringBuilder(); destBuilder.append(jingleConnection.getSessionId()); - if (initating) { + if (candidate.isOurs()) { destBuilder.append(jingleConnection.getAccountJid()); destBuilder.append(jingleConnection.getCounterPart()); } else { @@ -62,7 +53,7 @@ public class SocksConnection { @Override public void run() { try { - socket = new Socket(host, port); + socket = new Socket(candidate.getHost(), candidate.getPort()); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); byte[] login = { 0x05, 0x01, 0x00 }; @@ -78,8 +69,7 @@ public class SocksConnection { inputStream.read(result); int status = result[1]; if (status == 0) { - Log.d("xmppService", "established connection with "+host + ":" + port - + "/" + destination); + Log.d("xmppService", "established connection with "+candidate.getHost()+":"+candidate.getPort()+ "/" + destination); isEstablished = true; callback.established(); } else { @@ -193,24 +183,16 @@ public class SocksConnection { } public boolean isProxy() { - return this.isProxy; - } - - public String getJid() { - return this.jid; - } - - public String getCid() { - return this.cid; + return this.candidate.getType() == JingleCandidate.TYPE_PROXY; } public void disconnect() { if (this.socket!=null) { try { this.socket.close(); - Log.d("xmppService","cloesd socket with "+this.host); + Log.d("xmppService","cloesd socket with "+candidate.getHost()+":"+candidate.getPort()); } catch (IOException e) { - Log.d("xmppService","error closing socket with "+this.host); + Log.d("xmppService","error closing socket with "+candidate.getHost()+":"+candidate.getPort()); } } } @@ -218,4 +200,8 @@ public class SocksConnection { public boolean isEstablished() { return this.isEstablished; } + + public JingleCandidate getCandidate() { + return this.candidate; + } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index 79e04610a..3cd302516 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -77,6 +77,14 @@ public class Content extends Element { } } + public boolean hasCandidateError() { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + return false; + } + return transport.hasChild("candidate-error"); + } + public void setUsedCandidate(String transportId, String cid) { Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); if (transport==null) { @@ -95,4 +103,22 @@ public class Content extends Element { } transport.addChild(candidate); } + + public void setFileOffer(Element fileOffer) { + Element description = this.findChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); + if (description==null) { + description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); + } + description.addChild(fileOffer); + } + + public void setCandidateError(String transportId) { + Element transport = this.findChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + if (transport==null) { + transport = this.addChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + transport.setAttribute("sid", transportId); + transport.clearChildren(); + transport.addChild("candidate-error"); + } } |