From 18af5aa327bc0017e251488040145c88c7e6b99a Mon Sep 17 00:00:00 2001 From: Peter de Lange Date: Wed, 1 Jun 2016 16:38:20 +0200 Subject: [PATCH 1/6] description of node launcher tool --- src/main/java/i5/las2peer/tools/L2pNodeLauncher.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/i5/las2peer/tools/L2pNodeLauncher.java b/src/main/java/i5/las2peer/tools/L2pNodeLauncher.java index a821e2b03..c03556df1 100644 --- a/src/main/java/i5/las2peer/tools/L2pNodeLauncher.java +++ b/src/main/java/i5/las2peer/tools/L2pNodeLauncher.java @@ -51,7 +51,11 @@ import rice.p2p.commonapi.NodeHandle; /** - * This class implements the LAS2peer node launcher functionalities. + * las2peer node launcher + * + * It is the main tool for service developers / node maintainers to start their services, upload agents to the network + * and to test their service methods directly in the p2p network. The launcher both supports the start of a new network + * as well as starting a node that connects to an already existing network via a bootstrap. * * All methods to be executed can be stated via additional command line parameters to the {@link #main} method. */ From 6153bbcf26bfde62e910eea422dc05815ddd30c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Cuj=C3=A9?= Date: Fri, 3 Jun 2016 10:57:04 +0200 Subject: [PATCH 2/6] formatting --- .../las2peer/security/UserAgentManager.java | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/main/java/i5/las2peer/security/UserAgentManager.java b/src/main/java/i5/las2peer/security/UserAgentManager.java index 5a756ee71..69c4e9af0 100644 --- a/src/main/java/i5/las2peer/security/UserAgentManager.java +++ b/src/main/java/i5/las2peer/security/UserAgentManager.java @@ -18,16 +18,16 @@ * */ public class UserAgentManager { - + private static final String PREFIX_USER_NAME = "USER_NAME-"; private static final String PREFIX_USER_MAIL = "USER_MAIL-"; - - private Node node; - + + private Node node; + public UserAgentManager(Node node) { this.node = node; } - + /** * Stores login name and email of an user agent to the network * @@ -36,15 +36,17 @@ public UserAgentManager(Node node) { * @throws DuplicateLoginNameException * @throws AgentLockedException */ - public void registerUserAgent(UserAgent agent) throws DuplicateEmailException, DuplicateLoginNameException, AgentLockedException { + public void registerUserAgent(UserAgent agent) + throws DuplicateEmailException, DuplicateLoginNameException, AgentLockedException { if (agent.isLocked()) throw new AgentLockedException("Only unlocked Agents can be registered!"); - + Long content = agent.getId(); - + if (agent.hasLogin()) { try { - Envelope envName = Envelope.createClassIdEnvelope(content, PREFIX_USER_NAME + agent.getLoginName().toLowerCase(), agent); + Envelope envName = Envelope.createClassIdEnvelope(content, + PREFIX_USER_NAME + agent.getLoginName().toLowerCase(), agent); envName.open(agent); envName.setOverWriteBlindly(true); envName.addReader(node.getAnonymous()); @@ -53,15 +55,16 @@ public void registerUserAgent(UserAgent agent) throws DuplicateEmailException, D envName.close(); } catch (L2pSecurityException e) { throw new DuplicateLoginNameException(); - } catch (UnsupportedEncodingException | EncodingFailedException - | SerializationException | StorageException | DecodingFailedException e) { + } catch (UnsupportedEncodingException | EncodingFailedException | SerializationException | StorageException + | DecodingFailedException e) { node.observerNotice(Event.NODE_ERROR, "Envelope error while updating user list: " + e); } } - + if (agent.hasEmail()) { try { - Envelope envMail = Envelope.createClassIdEnvelope(content, PREFIX_USER_MAIL + agent.getEmail().toLowerCase(), agent); + Envelope envMail = Envelope.createClassIdEnvelope(content, + PREFIX_USER_MAIL + agent.getEmail().toLowerCase(), agent); envMail.open(agent); envMail.setOverWriteBlindly(true); envMail.addReader(node.getAnonymous()); @@ -70,56 +73,62 @@ public void registerUserAgent(UserAgent agent) throws DuplicateEmailException, D envMail.close(); } catch (L2pSecurityException e) { throw new DuplicateEmailException(); - } catch (UnsupportedEncodingException | EncodingFailedException - | SerializationException | StorageException | DecodingFailedException e) { + } catch (UnsupportedEncodingException | EncodingFailedException | SerializationException | StorageException + | DecodingFailedException e) { node.observerNotice(Event.NODE_ERROR, "Envelope error while updating user list: " + e); } } } - + /** * updates login name and email of an user + * * @param agent * @throws AgentLockedException - * @throws DuplicateEmailException - * @throws DuplicateLoginNameException + * @throws DuplicateEmailException + * @throws DuplicateLoginNameException */ - public void updateUserAgent(UserAgent agent) throws AgentLockedException, DuplicateEmailException, DuplicateLoginNameException { + public void updateUserAgent(UserAgent agent) + throws AgentLockedException, DuplicateEmailException, DuplicateLoginNameException { registerUserAgent(agent); } - + /** * get an {@link UserAgent}'s id by login name + * * @param name * @return * @throws AgentNotKnownException */ public long getAgentIdByLogin(String name) throws AgentNotKnownException { try { - Envelope env = node.fetchArtifact(Envelope.getClassEnvelopeId(Long.class, PREFIX_USER_NAME + name.toLowerCase())); + Envelope env = node + .fetchArtifact(Envelope.getClassEnvelopeId(Long.class, PREFIX_USER_NAME + name.toLowerCase())); env.open(node.getAnonymous()); Long content = env.getContent(Long.class); env.close(); - + return content; } catch (StorageException | ArtifactNotFoundException | EnvelopeException | L2pSecurityException e) { throw new AgentNotKnownException("Username not found!", e); } } - + /** * get an {@link UserAgent}'s id by email address + * * @param email * @return * @throws AgentNotKnownException */ public long getAgentIdByEmail(String email) throws AgentNotKnownException { try { - Envelope env = node.fetchArtifact(Envelope.getClassEnvelopeId(Long.class, PREFIX_USER_MAIL + email.toLowerCase())); + Envelope env = node + .fetchArtifact(Envelope.getClassEnvelopeId(Long.class, PREFIX_USER_MAIL + email.toLowerCase())); env.open(node.getAnonymous()); Long content = env.getContent(Long.class); env.close(); - + return content; } catch (StorageException | ArtifactNotFoundException | EnvelopeException | L2pSecurityException e) { throw new AgentNotKnownException("Email not found!", e); From 780414df7f169b806c4fcfce464120be14061da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Cuj=C3=A9?= Date: Fri, 3 Jun 2016 10:59:56 +0200 Subject: [PATCH 3/6] remove obsolete UnsupportedEncodingException use StandardCharsets --- .../i5/las2peer/persistency/Envelope.java | 35 +++++++------------ .../las2peer/security/UserAgentManager.java | 8 ++--- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/main/java/i5/las2peer/persistency/Envelope.java b/src/main/java/i5/las2peer/persistency/Envelope.java index beeb55e1b..59e623aca 100644 --- a/src/main/java/i5/las2peer/persistency/Envelope.java +++ b/src/main/java/i5/las2peer/persistency/Envelope.java @@ -5,7 +5,7 @@ import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.io.Serializable; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; @@ -211,12 +211,11 @@ private void initReaders(Agent[] readers) throws EncodingFailedException { * @param content * @param reader * - * @throws UnsupportedEncodingException * @throws EncodingFailedException * @throws DecodingFailedException */ public Envelope(String content, Agent reader) - throws UnsupportedEncodingException, EncodingFailedException, DecodingFailedException { + throws EncodingFailedException, DecodingFailedException { this(content, new Agent[] { reader }); } @@ -228,10 +227,9 @@ public Envelope(String content, Agent reader) * @param content * @param readers * - * @throws UnsupportedEncodingException * @throws EncodingFailedException */ - public Envelope(String content, Agent[] readers) throws UnsupportedEncodingException, EncodingFailedException { + public Envelope(String content, Agent[] readers) throws EncodingFailedException { this(content, readers, new Random().nextLong()); } @@ -243,11 +241,10 @@ public Envelope(String content, Agent[] readers) throws UnsupportedEncodingExcep * @param content * @param readers * @param id - * @throws UnsupportedEncodingException * @throws EncodingFailedException */ private Envelope(String content, Agent[] readers, long id) - throws UnsupportedEncodingException, EncodingFailedException { + throws EncodingFailedException { this.id = id; initKey(); @@ -270,10 +267,9 @@ private Envelope(String content, Agent[] readers, long id) * @param content * @param readers * - * @throws UnsupportedEncodingException * @throws EncodingFailedException */ - public Envelope(XmlAble content, Agent[] readers) throws UnsupportedEncodingException, EncodingFailedException { + public Envelope(XmlAble content, Agent[] readers) throws EncodingFailedException { this(content, readers, new Random().nextLong()); } @@ -288,10 +284,9 @@ public Envelope(XmlAble content, Agent[] readers) throws UnsupportedEncodingExce * @param readers * @param id * @throws EncodingFailedException - * @throws UnsupportedEncodingException */ private Envelope(XmlAble content, Agent[] readers, long id) - throws UnsupportedEncodingException, EncodingFailedException { + throws EncodingFailedException { this.id = id; initKey(); @@ -703,9 +698,7 @@ public String getContentAsString() throws EnvelopeException { byte[] content = null; try { content = getContentAsBinary(); - return new String(content, "UTF-8"); - } catch (UnsupportedEncodingException e) { - return new String(content); + return new String(content, StandardCharsets.UTF_8); } catch (Exception e) { throw new EnvelopeException("Coding problems with interpreting the content", e); } @@ -1129,10 +1122,9 @@ public static long getClassEnvelopeId(String cls, String identifier) { * * @throws EncodingFailedException * @throws SerializationException - * @throws UnsupportedEncodingException */ public static Envelope createClassIdEnvelope(Object content, String identifier, Agent[] readers) - throws EncodingFailedException, SerializationException, UnsupportedEncodingException { + throws EncodingFailedException, SerializationException { if (content instanceof String) return new Envelope((String) content, readers, getClassEnvelopeId(content.getClass(), identifier)); else if (content instanceof XmlAble) @@ -1156,10 +1148,9 @@ else if (content instanceof byte[]) * * @throws SerializationException * @throws EncodingFailedException - * @throws UnsupportedEncodingException */ public static Envelope createClassIdEnvelope(Object content, String identifier, Agent reader) - throws UnsupportedEncodingException, EncodingFailedException, SerializationException { + throws EncodingFailedException, SerializationException { return createClassIdEnvelope(content, identifier, new Agent[] { reader }); } @@ -1239,11 +1230,10 @@ public void updateContent(byte[] content) throws L2pSecurityException { * * @param content * - * @throws UnsupportedEncodingException * @throws L2pSecurityException */ - public void updateContent(String content) throws UnsupportedEncodingException, L2pSecurityException { - updateContent(content.getBytes("UTF-8")); + public void updateContent(String content) throws L2pSecurityException { + updateContent(content.getBytes(StandardCharsets.UTF_8)); contentType = ContentType.String; } @@ -1265,10 +1255,9 @@ public void updateContent(Serializable content) throws L2pSecurityException, Ser * * @param content * @throws L2pSecurityException - * @throws UnsupportedEncodingException * @throws SerializationException */ - public void updateContent(XmlAble content) throws UnsupportedEncodingException, L2pSecurityException, SerializationException { + public void updateContent(XmlAble content) throws L2pSecurityException, SerializationException { updateContent(content.toXmlString()); contentType = ContentType.XmlAble; clContentClass = content.getClass(); diff --git a/src/main/java/i5/las2peer/security/UserAgentManager.java b/src/main/java/i5/las2peer/security/UserAgentManager.java index 69c4e9af0..5fe38415d 100644 --- a/src/main/java/i5/las2peer/security/UserAgentManager.java +++ b/src/main/java/i5/las2peer/security/UserAgentManager.java @@ -1,7 +1,5 @@ package i5.las2peer.security; -import java.io.UnsupportedEncodingException; - import i5.las2peer.logging.NodeObserver.Event; import i5.las2peer.p2p.AgentNotKnownException; import i5.las2peer.p2p.ArtifactNotFoundException; @@ -55,8 +53,7 @@ public void registerUserAgent(UserAgent agent) envName.close(); } catch (L2pSecurityException e) { throw new DuplicateLoginNameException(); - } catch (UnsupportedEncodingException | EncodingFailedException | SerializationException | StorageException - | DecodingFailedException e) { + } catch (EncodingFailedException | SerializationException | StorageException | DecodingFailedException e) { node.observerNotice(Event.NODE_ERROR, "Envelope error while updating user list: " + e); } } @@ -73,8 +70,7 @@ public void registerUserAgent(UserAgent agent) envMail.close(); } catch (L2pSecurityException e) { throw new DuplicateEmailException(); - } catch (UnsupportedEncodingException | EncodingFailedException | SerializationException | StorageException - | DecodingFailedException e) { + } catch (EncodingFailedException | SerializationException | StorageException | DecodingFailedException e) { node.observerNotice(Event.NODE_ERROR, "Envelope error while updating user list: " + e); } } From 178b36ac610cf7fb22d220f67867a87807825792 Mon Sep 17 00:00:00 2001 From: Jasper Nalbach Date: Thu, 9 Jun 2016 11:21:48 +0200 Subject: [PATCH 4/6] added method to get reader from an envelope --- .../i5/las2peer/persistency/Envelope.java | 110 +++++++++++------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/src/main/java/i5/las2peer/persistency/Envelope.java b/src/main/java/i5/las2peer/persistency/Envelope.java index 59e623aca..0cb14ec2e 100644 --- a/src/main/java/i5/las2peer/persistency/Envelope.java +++ b/src/main/java/i5/las2peer/persistency/Envelope.java @@ -1,20 +1,5 @@ package i5.las2peer.persistency; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectStreamClass; -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Random; - -import javax.crypto.SecretKey; - -import org.apache.commons.codec.binary.Base64; - import i5.las2peer.execution.L2pThread; import i5.las2peer.p2p.ArtifactNotFoundException; import i5.las2peer.p2p.StorageException; @@ -32,6 +17,21 @@ import i5.simpleXML.Parser; import i5.simpleXML.XMLSyntaxException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Random; + +import javax.crypto.SecretKey; + +import org.apache.commons.codec.binary.Base64; + /** * An envelope provides a secure storage for any {@link Serializable} content within the LAS2peer network. * @@ -214,8 +214,7 @@ private void initReaders(Agent[] readers) throws EncodingFailedException { * @throws EncodingFailedException * @throws DecodingFailedException */ - public Envelope(String content, Agent reader) - throws EncodingFailedException, DecodingFailedException { + public Envelope(String content, Agent reader) throws EncodingFailedException, DecodingFailedException { this(content, new Agent[] { reader }); } @@ -243,8 +242,7 @@ public Envelope(String content, Agent[] readers) throws EncodingFailedException * @param id * @throws EncodingFailedException */ - private Envelope(String content, Agent[] readers, long id) - throws EncodingFailedException { + private Envelope(String content, Agent[] readers, long id) throws EncodingFailedException { this.id = id; initKey(); @@ -285,8 +283,7 @@ public Envelope(XmlAble content, Agent[] readers) throws EncodingFailedException * @param id * @throws EncodingFailedException */ - private Envelope(XmlAble content, Agent[] readers, long id) - throws EncodingFailedException { + private Envelope(XmlAble content, Agent[] readers, long id) throws EncodingFailedException { this.id = id; initKey(); @@ -310,7 +307,7 @@ private Envelope(XmlAble content, Agent[] readers, long id) * * @param content * @param reader - * @throws EnvelopeException + * @throws EnvelopeException * * @throws EncodingFailedException * @throws SerializationException @@ -348,8 +345,8 @@ public Envelope(Serializable content, Agent[] readers) throws EncodingFailedExce * @throws EncodingFailedException * @throws SerializationException */ - private Envelope(Serializable content, Agent[] readers, long id) - throws EncodingFailedException, SerializationException { + private Envelope(Serializable content, Agent[] readers, long id) throws EncodingFailedException, + SerializationException { this.id = id; initKey(); @@ -429,7 +426,7 @@ public void open(Agent agent) throws DecodingFailedException, L2pSecurityExcepti throw new L2pSecurityException("agent " + agent.getId() + " has no access to this object"); } - symmetricKey = (SecretKey) agent.returnSecretKey(encoded); + symmetricKey = agent.returnSecretKey(encoded); openedBy = agent; decryptData(); @@ -556,6 +553,21 @@ public void removeReader(Agent agent) throws L2pSecurityException { htEncryptedKeys.remove(agent.getId()); } + /** + * checks if an agent is reader + * + * Attention: only direct reading access will be checked, no access gained via group memberships + * + * @param agent agent to check + * @return true if and only if the given agent is a reader + */ + public boolean hasReader(Agent agent) { + if (agent instanceof GroupAgent) + return htEncryptedGroupKeys.contains(agent.getId()); + else + return htEncryptedKeys.contains(agent.getId()); + } + /** * add a signature for the content. only agents that signed the Evnelope have writing access. if no signature is * given, every reader can write to the envelope. @@ -690,7 +702,7 @@ public byte[] getContentAsBinary() throws DecodingFailedException { * returns the contents of this envelope as string * * @return content as string - * @throws EnvelopeException + * @throws EnvelopeException * * @throws DecodingFailedException */ @@ -736,6 +748,15 @@ public XmlAble getContentAsXmlAble() throws EnvelopeException { } } + /** + * get a list with all ids of non-group agents entitled to read this envelope + * + * @return array with all agent ids + */ + public Long[] getReader() { + return htEncryptedKeys.keySet().toArray(new Long[0]); + } + /** * get a list with all ids of groups entitled to read this envelope * @@ -773,7 +794,8 @@ public Serializable getContentAsSerializable() throws EnvelopeException { /** * Get the content as deserialized object. This method uses the same class loader as the calling class. - * @param + * + * @param * * @param cls * @return the typed content of this envelope @@ -792,7 +814,8 @@ public T getContent(Class cls) throws EnvelopeExcept /** * Get the content as deserialized object. - * @param + * + * @param * * @param cls * @param classLoader @@ -830,8 +853,9 @@ protected Class resolveClass(ObjectStreamClass desc) throws IOException, Clas /** * @return a XML (string) representation of this envelope - * @throws SerializationException + * @throws SerializationException */ + @Override public String toXmlString() throws SerializationException { if (baPlainData != null && baCipherData == null) { try { @@ -841,8 +865,8 @@ public String toXmlString() throws SerializationException { } } - String encodedKeys = "\t\n"; + String encodedKeys = "\t\n"; for (Long id : htEncryptedKeys.keySet()) { encodedKeys += "\t\t" + Base64.encodeBase64String(htEncryptedKeys.get(id)) + "\n"; @@ -987,11 +1011,11 @@ public static Envelope createFromXml(Element root) throws MalformedXMLException if (!keys.getName().equals("keys")) throw new MalformedXMLException("not an envelope"); if (!keys.getAttribute("encoding").equals("base64")) - throw new MalformedXMLException( - "base 64 encoding of the content expected - got: " + keys.getAttribute("encoding")); + throw new MalformedXMLException("base 64 encoding of the content expected - got: " + + keys.getAttribute("encoding")); if (!keys.getAttribute("encryption").equals(CryptoTools.getAsymmetricAlgorithm())) - throw new MalformedXMLException( - CryptoTools.getAsymmetricAlgorithm() + " encryption of the content expected"); + throw new MalformedXMLException(CryptoTools.getAsymmetricAlgorithm() + + " encryption of the content expected"); for (Enumeration enKeys = keys.getChildren(); enKeys.hasMoreElements();) { Element key = enKeys.nextElement(); @@ -1012,8 +1036,8 @@ public static Envelope createFromXml(Element root) throws MalformedXMLException if (!signatures.getName().equals("signatures")) throw new MalformedXMLException("signatures expected"); if (!signatures.getAttribute("encoding").equals("base64")) - throw new MalformedXMLException( - "base 64 encoding of the content expected - got: " + keys.getAttribute("encoding")); + throw new MalformedXMLException("base 64 encoding of the content expected - got: " + + keys.getAttribute("encoding")); if (!signatures.getAttribute("method").equals(CryptoTools.getSignatureMethod())) throw new MalformedXMLException(CryptoTools.getSignatureMethod() + " expected as signature method"); @@ -1038,7 +1062,7 @@ public static Envelope createFromXml(Element root) throws MalformedXMLException * get a locked copy of this agent * * @return a locked clone of this envelope - * @throws EnvelopeException + * @throws EnvelopeException * @throws EncodingFailedException */ public final Envelope cloneLocked() throws EnvelopeException { @@ -1179,8 +1203,8 @@ public static Envelope fetch(long id) throws ArtifactNotFoundException, StorageE * @throws ArtifactNotFoundException * @throws StorageException */ - public static Envelope fetchClassIdEnvelope(Class cls, String identifier) - throws ArtifactNotFoundException, StorageException { + public static Envelope fetchClassIdEnvelope(Class cls, String identifier) throws ArtifactNotFoundException, + StorageException { return Context.getCurrent().getStoredObject(cls, identifier); } @@ -1255,7 +1279,7 @@ public void updateContent(Serializable content) throws L2pSecurityException, Ser * * @param content * @throws L2pSecurityException - * @throws SerializationException + * @throws SerializationException */ public void updateContent(XmlAble content) throws L2pSecurityException, SerializationException { updateContent(content.toXmlString()); @@ -1286,8 +1310,8 @@ public void checkOverwrite(Envelope envelope) throws L2pSecurityException { return; } - throw new L2pSecurityException( - "Check for Overwriting envelope " + getId() + " failed: No needed signature is provided!"); + throw new L2pSecurityException("Check for Overwriting envelope " + getId() + + " failed: No needed signature is provided!"); } /** From b5e7442d7aca395284b2ce6cf01235d67fe31313 Mon Sep 17 00:00:00 2001 From: Jasper Nalbach Date: Thu, 9 Jun 2016 15:36:32 +0200 Subject: [PATCH 5/6] fixed last commit --- src/main/java/i5/las2peer/persistency/Envelope.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/i5/las2peer/persistency/Envelope.java b/src/main/java/i5/las2peer/persistency/Envelope.java index 0cb14ec2e..e687c0705 100644 --- a/src/main/java/i5/las2peer/persistency/Envelope.java +++ b/src/main/java/i5/las2peer/persistency/Envelope.java @@ -563,9 +563,9 @@ public void removeReader(Agent agent) throws L2pSecurityException { */ public boolean hasReader(Agent agent) { if (agent instanceof GroupAgent) - return htEncryptedGroupKeys.contains(agent.getId()); + return htEncryptedGroupKeys.containsKey(agent.getId()); else - return htEncryptedKeys.contains(agent.getId()); + return htEncryptedKeys.containsKey(agent.getId()); } /** From e1757f0d41d8f43301e720e3236bdeb06d6da9b9 Mon Sep 17 00:00:00 2001 From: Jasper Nalbach Date: Fri, 10 Jun 2016 23:21:20 +0200 Subject: [PATCH 6/6] increase build number --- build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.xml b/build.xml index 4f9735d47..d7f85278c 100644 --- a/build.xml +++ b/build.xml @@ -5,7 +5,7 @@ - +