Completed basic support for NFC / NDEF formats applicable to 2D barcodes. Not yet...
authorsrowen <srowen@59b500cc-1b3d-0410-9834-0bbf25fbcc57>
Mon, 31 Mar 2008 20:51:24 +0000 (20:51 +0000)
committersrowen <srowen@59b500cc-1b3d-0410-9834-0bbf25fbcc57>
Mon, 31 Mar 2008 20:51:24 +0000 (20:51 +0000)
git-svn-id: http://zxing.googlecode.com/svn/trunk@329 59b500cc-1b3d-0410-9834-0bbf25fbcc57

18 files changed:
core/src/com/google/zxing/client/result/AbstractNDEFParsedResult.java
core/src/com/google/zxing/client/result/AddressBookAUParsedResult.java
core/src/com/google/zxing/client/result/AddressBookDoCoMoParsedResult.java
core/src/com/google/zxing/client/result/BookmarkDoCoMoParsedResult.java
core/src/com/google/zxing/client/result/EmailAddressParsedResult.java
core/src/com/google/zxing/client/result/EmailDoCoMoParsedResult.java
core/src/com/google/zxing/client/result/GeoParsedResult.java
core/src/com/google/zxing/client/result/NDEFRecord.java [new file with mode: 0644]
core/src/com/google/zxing/client/result/NDEFSmartPosterParsedResult.java [new file with mode: 0644]
core/src/com/google/zxing/client/result/NDEFTextParsedResult.java
core/src/com/google/zxing/client/result/NDEFURIParsedResult.java
core/src/com/google/zxing/client/result/ParsedReaderResult.java
core/src/com/google/zxing/client/result/ParsedReaderResultType.java
core/src/com/google/zxing/client/result/TelParsedResult.java
core/src/com/google/zxing/client/result/UPCParsedResult.java
core/src/com/google/zxing/client/result/URIParsedResult.java
core/src/com/google/zxing/client/result/URLTOParsedResult.java
core/test/src/com/google/zxing/client/result/ParsedReaderResultTestCase.java

index e6433ef..8cd2493 100644 (file)
@@ -30,35 +30,16 @@ import java.io.UnsupportedEncodingException;
  */
 abstract class AbstractNDEFParsedResult extends ParsedReaderResult {
 
-  /**
-   * MB  = 1 (start of record)
-   * ME  = 1 (also end of record)
-   * CF  = 0 (not a chunk)
-   * SR  = 1 (assume short record)
-   * ID  = 0 (ID length field omitted)
-   * TNF = 0 (= 1, well-known type)
-   *       0
-   *       1
-   */
-  private static final int HEADER_VALUE = 0xD1;
-  private static final int MASK = 0xFF;
-
   AbstractNDEFParsedResult(ParsedReaderResultType type) {
     super(type);
   }
 
-  static boolean isMaybeNDEF(byte[] bytes) {
-    return
-        bytes != null &&
-        bytes.length >= 4 &&
-        ((bytes[0] & MASK) == HEADER_VALUE) && 
-        ((bytes[1] & 0xFF) == 1);
-  }
-
   static String bytesToString(byte[] bytes, int offset, int length, String encoding) {
     try {
       return new String(bytes, offset, length, encoding);
     } catch (UnsupportedEncodingException uee) {
+      // This should only be used when 'encoding' is an encoding that must necessarily
+      // be supported by the JVM, like UTF-8
       throw new RuntimeException("Platform does not support required encoding: " + uee);
     }
   }
index fba263b..84fdf48 100644 (file)
@@ -48,7 +48,7 @@ public final class AddressBookAUParsedResult extends ParsedReaderResult {
   public static AddressBookAUParsedResult parse(Result result) {
     String rawText = result.getText();
     // MEMORY is mandatory; seems like a decent indicator, as does end-of-record separator CR/LF
-    if (rawText.indexOf("MEMORY") < 0 || rawText.indexOf("\r\n") < 0) {
+    if (rawText == null || rawText.indexOf("MEMORY") < 0 || rawText.indexOf("\r\n") < 0) {
       return null;
     }
     String[] names = matchMultipleValuePrefix("NAME", 2, rawText);
index e85b6f4..5c2f6b8 100644 (file)
@@ -47,7 +47,7 @@ public final class AddressBookDoCoMoParsedResult extends AbstractDoCoMoParsedRes
 
   public static AddressBookDoCoMoParsedResult parse(Result result) {
     String rawText = result.getText();
-    if (!rawText.startsWith("MECARD:")) {
+    if (rawText == null || !rawText.startsWith("MECARD:")) {
       return null;
     }
     String[] rawName = matchPrefixedField("N:", rawText);
index f170300..d552db0 100644 (file)
@@ -34,7 +34,7 @@ public final class BookmarkDoCoMoParsedResult extends AbstractDoCoMoParsedResult
 
   public static BookmarkDoCoMoParsedResult parse(Result result) {
     String rawText = result.getText();
-    if (!rawText.startsWith("MEBKM:")) {
+    if (rawText == null || !rawText.startsWith("MEBKM:")) {
       return null;
     }
     String title = matchSinglePrefixedField("TITLE:", rawText);
index d515c46..fc334b6 100644 (file)
@@ -36,7 +36,7 @@ public final class EmailAddressParsedResult extends AbstractDoCoMoParsedResult {
   public static EmailAddressParsedResult parse(Result result) {
     String rawText = result.getText();
     String emailAddress;
-    if (rawText.startsWith("mailto:")) {
+    if (rawText != null && rawText.startsWith("mailto:")) {
       // If it starts with mailto:, assume it is definitely trying to be an email address
       emailAddress = rawText.substring(7);
     } else {
index 437681a..d410e19 100644 (file)
@@ -40,7 +40,7 @@ public final class EmailDoCoMoParsedResult extends AbstractDoCoMoParsedResult {
 
   public static EmailDoCoMoParsedResult parse(Result result) {
     String rawText = result.getText();
-    if (!rawText.startsWith("MATMSG:")) {
+    if (rawText == null || !rawText.startsWith("MATMSG:")) {
       return null;
     }
     String[] rawTo = matchPrefixedField("TO:", rawText);
@@ -82,6 +82,9 @@ public final class EmailDoCoMoParsedResult extends AbstractDoCoMoParsedResult {
    * in a barcode, not "judge" it.
    */
   static boolean isBasicallyValidEmailAddress(String email) {
+    if (email == null) {
+      return false;
+    }
     int atIndex = email.indexOf('@');
     return atIndex >= 0 && email.indexOf('.') > atIndex && email.indexOf(' ') < 0;
   }
index 8cbb3be..c4b7cbc 100644 (file)
@@ -43,7 +43,7 @@ public final class GeoParsedResult extends ParsedReaderResult {
 
   public static GeoParsedResult parse(Result result) {
     String rawText = result.getText();
-    if (!rawText.startsWith("geo:")) {
+    if (rawText == null || !rawText.startsWith("geo:")) {
       return null;
     }
     // Drop geo, query portion
diff --git a/core/src/com/google/zxing/client/result/NDEFRecord.java b/core/src/com/google/zxing/client/result/NDEFRecord.java
new file mode 100644 (file)
index 0000000..59797c2
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.result;
+
+/**
+ * <p>Represents a record in an NDEF message. This class only supports certain types
+ * of records -- namely, non-chunked records, where ID length is omitted, and only
+ * "short records".</p>
+ *
+ * @author srowen@google.com (Sean Owen)
+ */
+final class NDEFRecord {
+
+  private static final int SUPPORTED_HEADER_MASK = 0x3F; // 0 0 1 1 1 111 (the bottom 6 bits matter)
+  private static final int SUPPORTED_HEADER = 0x11;      // 0 0 0 1 0 001
+
+  public static final String TEXT_WELL_KNOWN_TYPE = "T";
+  public static final String URI_WELL_KNOWN_TYPE = "U";
+  public static final String SMART_POSTER_WELL_KNOWN_TYPE = "Sp";
+  public static final String ACTION_WELL_KNOWN_TYPE = "act";
+
+  private final int header;
+  private final String type;
+  private final byte[] payload;
+  private final int totalRecordLength;
+
+  private NDEFRecord(int header, String type, byte[] payload, int totalRecordLength) {
+    this.header = header;
+    this.type = type;
+    this.payload = payload;
+    this.totalRecordLength = totalRecordLength;
+  }
+
+  static NDEFRecord readRecord(byte[] bytes, int offset) {
+    int header = bytes[offset] & 0xFF;
+    // Does header match what we support in the bits we care about?
+    // XOR figures out where we differ, and if any of those are in the mask, fail
+    if (((header ^ SUPPORTED_HEADER) & SUPPORTED_HEADER_MASK) != 0) {
+      return null;
+    }
+    int typeLength = bytes[offset + 1] & 0xFF;
+
+    int payloadLength = bytes[offset + 2] & 0xFF;
+
+    String type = AbstractNDEFParsedResult.bytesToString(bytes, offset + 3, typeLength, "US-ASCII");
+
+    byte[] payload = new byte[payloadLength];
+    System.arraycopy(bytes, offset + 3 + typeLength, payload, 0, payloadLength);
+
+    return new NDEFRecord(header, type, payload, 3 + typeLength + payloadLength);
+  }
+
+  boolean isMessageBegin() {
+    return (header & 0x80) != 0;
+  }
+
+  boolean isMessageEnd() {
+    return (header & 0x40) != 0;
+  }
+
+  String getType() {
+    return type;
+  }
+
+  byte[] getPayload() {
+    return payload;
+  }
+
+  int getTotalRecordLength() {
+    return totalRecordLength;
+  }
+
+}
\ No newline at end of file
diff --git a/core/src/com/google/zxing/client/result/NDEFSmartPosterParsedResult.java b/core/src/com/google/zxing/client/result/NDEFSmartPosterParsedResult.java
new file mode 100644 (file)
index 0000000..bb20f7e
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.result;
+
+import com.google.zxing.Result;
+
+/**
+ * <p>Recognizes an NDEF message that encodes information according to the
+ * "Smart Poster Record Type Definition" specification.</p>
+ *
+ * <p>This actually only supports some parts of the Smart Poster format: title,
+ * URI, and action records. Icon records are not supported because the size
+ * of these records are infeasibly large for barcodes. Size and type records
+ * are not supported. Multiple titles are not supported.</p>
+ *
+ * @author srowen@google.com (Sean Owen)
+ */
+public final class NDEFSmartPosterParsedResult extends AbstractNDEFParsedResult {
+
+  public static final int ACTION_UNSPECIFIED = -1;
+  public static final int ACTION_DO = 0;
+  public static final int ACTION_SAVE = 1;
+  public static final int ACTION_OPEN = 2;
+
+  private String title;
+  private String uri;
+  private int action;
+
+  private NDEFSmartPosterParsedResult() {
+    super(ParsedReaderResultType.NDEF_SMART_POSTER);
+    action = ACTION_UNSPECIFIED;
+  }
+
+  public static NDEFSmartPosterParsedResult parse(Result result) {
+    byte[] bytes = result.getRawBytes();
+    if (bytes == null) {
+      return null;
+    }
+    NDEFRecord headerRecord = NDEFRecord.readRecord(bytes, 0);
+    // Yes, header record starts and ends a message
+    if (headerRecord == null || !headerRecord.isMessageBegin() || !headerRecord.isMessageEnd()) {
+      return null;
+    }
+    if (!headerRecord.getType().equals(NDEFRecord.SMART_POSTER_WELL_KNOWN_TYPE)) {
+      return null;
+    }
+
+    int offset = 0;
+    int recordNumber = 0;
+    NDEFRecord ndefRecord = null;
+    byte[] payload = headerRecord.getPayload();
+    NDEFSmartPosterParsedResult smartPosterParsedResult = new NDEFSmartPosterParsedResult();
+
+    while (offset < payload.length && (ndefRecord = NDEFRecord.readRecord(payload, offset)) != null) {
+      if (recordNumber == 0 && !ndefRecord.isMessageBegin()) {
+        return null;
+      }
+      String type = ndefRecord.getType();
+      if (NDEFRecord.TEXT_WELL_KNOWN_TYPE.equals(type)) {
+        String[] languageText = NDEFTextParsedResult.decodeTextPayload(ndefRecord.getPayload());
+        smartPosterParsedResult.title = languageText[1];
+      } else if (NDEFRecord.URI_WELL_KNOWN_TYPE.equals(type)) {
+        smartPosterParsedResult.uri = NDEFURIParsedResult.decodeURIPayload(ndefRecord.getPayload());
+      } else if (NDEFRecord.ACTION_WELL_KNOWN_TYPE.equals(type)) {
+        smartPosterParsedResult.action = ndefRecord.getPayload()[0];
+      }
+      recordNumber++;
+      offset += ndefRecord.getTotalRecordLength();
+    }
+    
+    if (recordNumber == 0 || (ndefRecord != null && !ndefRecord.isMessageEnd())) {
+      return null;
+    }
+
+    return smartPosterParsedResult;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public String getURI() {
+    return uri;
+  }
+
+  public int getAction() {
+    return action;
+  }
+
+  public String getDisplayResult() {
+    if (title == null) {
+      return uri;
+    } else {
+      return title + '\n' + uri;
+    }
+  }
+
+}
\ No newline at end of file
index 05d12df..bec1fdd 100644 (file)
@@ -26,7 +26,6 @@ import com.google.zxing.Result;
  */
 public final class NDEFTextParsedResult extends AbstractNDEFParsedResult {
 
-  private static final byte TEXT_WELL_KNOWN_TYPE = (byte) 0x54;
 
   private final String language;
   private final String text;
@@ -39,27 +38,29 @@ public final class NDEFTextParsedResult extends AbstractNDEFParsedResult {
 
   public static NDEFTextParsedResult parse(Result result) {
     byte[] bytes = result.getRawBytes();
-    if (!isMaybeNDEF(bytes)) {
+    if (bytes == null) {
       return null;
     }
-
-    int payloadLength = bytes[2] & 0xFF;
-
-    // Next 1 byte is type
-    if (bytes[3] != TEXT_WELL_KNOWN_TYPE) {
+    NDEFRecord ndefRecord = NDEFRecord.readRecord(bytes, 0);
+    if (ndefRecord == null || !ndefRecord.isMessageBegin() || !ndefRecord.isMessageEnd()) {
+      return null;
+    }
+    if (!ndefRecord.getType().equals(NDEFRecord.TEXT_WELL_KNOWN_TYPE)) {
       return null;
     }
+    String[] languageText = decodeTextPayload(ndefRecord.getPayload());
+    return new NDEFTextParsedResult(languageText[0], languageText[1]);
+  }
 
-    // Text record
-    byte statusByte = bytes[4];
+  static String[] decodeTextPayload(byte[] payload) {
+    byte statusByte = payload[0];
     boolean isUTF16 = (statusByte & 0x80) != 0;
     int languageLength = statusByte & 0x1F;
-
     // language is always ASCII-encoded:
-    String language = bytesToString(bytes, 5, languageLength, "US-ASCII");
+    String language = bytesToString(payload, 1, languageLength, "US-ASCII");
     String encoding = isUTF16 ? "UTF-16" : "UTF-8";
-    String text = bytesToString(bytes, 5 + languageLength, payloadLength - languageLength - 1, encoding);
-    return new NDEFTextParsedResult(language, text);
+    String text = bytesToString(payload, 1 + languageLength, payload.length - languageLength - 1, encoding);
+    return new String[] { language, text };
   }
 
   public String getLanguage() {
index aa85a13..bb6d985 100644 (file)
@@ -26,8 +26,6 @@ import com.google.zxing.Result;
  */
 public final class NDEFURIParsedResult extends AbstractNDEFParsedResult {
 
-  private static final byte URI_WELL_KNOWN_TYPE = (byte) 0x55;
-
   private static final String[] URI_PREFIXES = new String[] {
       null,
       "http://www.",
@@ -70,32 +68,34 @@ public final class NDEFURIParsedResult extends AbstractNDEFParsedResult {
   private final String uri;
 
   private NDEFURIParsedResult(String uri) {
-    super(ParsedReaderResultType.NDEF_TEXT);
+    super(ParsedReaderResultType.NDEF_URI);
     this.uri = uri;
   }
 
   public static NDEFURIParsedResult parse(Result result) {
     byte[] bytes = result.getRawBytes();
-    if (!isMaybeNDEF(bytes)) {
+    if (bytes == null) {
       return null;
     }
-
-    int payloadLength = bytes[2] & 0xFF;
-
-    // Next 1 byte is type
-    if (bytes[3] != URI_WELL_KNOWN_TYPE) {
+    NDEFRecord ndefRecord = NDEFRecord.readRecord(bytes, 0);
+    if (ndefRecord == null || !ndefRecord.isMessageBegin() || !ndefRecord.isMessageEnd()) {
+      return null;
+    }
+    if (!ndefRecord.getType().equals(NDEFRecord.URI_WELL_KNOWN_TYPE)) {
       return null;
     }
+    String fullURI = decodeURIPayload(ndefRecord.getPayload());
+    return new NDEFURIParsedResult(fullURI);
+  }
 
-    int identifierCode = bytes[4] & 0xFF;
+  static String decodeURIPayload(byte[] payload) {
+    int identifierCode = payload[0] & 0xFF;
     String prefix = null;
     if (identifierCode < URI_PREFIXES.length) {
       prefix = URI_PREFIXES[identifierCode];
     }
-
-    String restOfURI = bytesToString(bytes, 5, payloadLength - 1, "UTF-8");
-    String fullURI = prefix == null ? restOfURI : prefix + restOfURI;
-    return new NDEFURIParsedResult(fullURI);
+    String restOfURI = bytesToString(payload, 1, payload.length - 1, "UTF-8");
+    return prefix == null ? restOfURI : prefix + restOfURI;
   }
 
   public String getURI() {
index 44a6c6b..f0c4909 100644 (file)
@@ -68,6 +68,12 @@ public abstract class ParsedReaderResult {
       return result;
     } else if ((result = UPCParsedResult.parse(theResult)) != null) {
       return result;
+    //} else if ((result = NDEFTextParsedResult.parse(theResult)) != null) {
+    //  return result;
+    //} else if ((result = NDEFURIParsedResult.parse(theResult)) != null) {
+    //  return result;
+    //} else if ((result = NDEFSmartPosterParsedResult.parse(theResult)) != null) {
+    //  return result;
     }
     return TextParsedResult.parse(theResult);
   }
index e7345c3..344fe4c 100644 (file)
@@ -39,6 +39,7 @@ public final class ParsedReaderResultType {
   // TODO later, add the NDEF types to those actually processed by the clients
   public static final ParsedReaderResultType NDEF_TEXT = new ParsedReaderResultType("NDEF_TEXT");
   public static final ParsedReaderResultType NDEF_URI = new ParsedReaderResultType("NDEF_URI");
+  public static final ParsedReaderResultType NDEF_SMART_POSTER = new ParsedReaderResultType("NDEF_SMART_POSTER");
 
   private final String name;
 
index 598861c..4b92786 100644 (file)
@@ -34,7 +34,7 @@ public final class TelParsedResult extends ParsedReaderResult {
 
   public static TelParsedResult parse(Result result) {
     String rawText = result.getText();
-    if (!rawText.startsWith("tel:")) {
+    if (rawText == null || !rawText.startsWith("tel:")) {
       return null;
     }
     // Drop tel, query portion
index 1287eaa..cb87611 100644 (file)
@@ -37,6 +37,9 @@ public final class UPCParsedResult extends ParsedReaderResult {
       return null;
     }
     String rawText = result.getText();
+    if (rawText == null) {
+      return null;
+    }
     int length = rawText.length();
     if (length != 12 && length != 13) {
       return null;
index 792acea..52c93cb 100644 (file)
@@ -74,7 +74,7 @@ public final class URIParsedResult extends ParsedReaderResult {
    * need to know when a string is obviously not a URI.
    */
   static boolean isBasicallyValidURI(String uri) {
-    return uri.indexOf(' ') < 0 && (uri.indexOf(':') >= 0 || uri.indexOf('.') >= 0);
+    return uri != null && uri.indexOf(' ') < 0 && (uri.indexOf(':') >= 0 || uri.indexOf('.') >= 0);
   }
 
 }
\ No newline at end of file
index f00d5f9..ab7ace2 100644 (file)
@@ -38,7 +38,7 @@ public final class URLTOParsedResult extends ParsedReaderResult {
 
   public static URLTOParsedResult parse(Result result) {
     String rawText = result.getText();
-    if (!rawText.startsWith("URLTO:")) {
+    if (rawText == null || !rawText.startsWith("URLTO:")) {
       return null;
     }
     int titleEnd = rawText.indexOf(':', 6);
index 68e33fc..f27962f 100644 (file)
@@ -106,6 +106,39 @@ public final class ParsedReaderResultTestCase extends TestCase {
     doTestResult("telephone", ParsedReaderResultType.TEXT);
   }
 
+  /*
+  public void testNDEFText() {
+    doTestResult(new byte[] {(byte)0xD1,(byte)0x01,(byte)0x05,(byte)0x54,
+                             (byte)0x02,(byte)0x65,(byte)0x6E,(byte)0x68,
+                             (byte)0x69},
+                 ParsedReaderResultType.NDEF_TEXT);
+  }
+
+  public void testNDEFURI() {
+    doTestResult(new byte[] {(byte)0xD1,(byte)0x01,(byte)0x08,(byte)0x55,
+                             (byte)0x01,(byte)0x6E,(byte)0x66,(byte)0x63,
+                             (byte)0x2E,(byte)0x63,(byte)0x6F,(byte)0x6D},
+                 ParsedReaderResultType.NDEF_URI);
+  }
+
+  public void testNDEFSmartPoster() {
+    doTestResult(new byte[] {(byte)0xD1,(byte)0x02,(byte)0x2F,(byte)0x53,
+                             (byte)0x70,(byte)0x91,(byte)0x01,(byte)0x0E,
+                             (byte)0x55,(byte)0x01,(byte)0x6E,(byte)0x66,
+                             (byte)0x63,(byte)0x2D,(byte)0x66,(byte)0x6F,
+                             (byte)0x72,(byte)0x75,(byte)0x6D,(byte)0x2E,
+                             (byte)0x6F,(byte)0x72,(byte)0x67,(byte)0x11,
+                             (byte)0x03,(byte)0x01,(byte)0x61,(byte)0x63,
+                             (byte)0x74,(byte)0x00,(byte)0x51,(byte)0x01,
+                             (byte)0x12,(byte)0x54,(byte)0x05,(byte)0x65,
+                             (byte)0x6E,(byte)0x2D,(byte)0x55,(byte)0x53,
+                             (byte)0x48,(byte)0x65,(byte)0x6C,(byte)0x6C,
+                             (byte)0x6F,(byte)0x2C,(byte)0x20,(byte)0x77,
+                             (byte)0x6F,(byte)0x72,(byte)0x6C,(byte)0x64},
+                 ParsedReaderResultType.NDEF_SMART_POSTER);
+  }
+   */
+
   private static void doTestResult(String text, ParsedReaderResultType type) {
     doTestResult(text, type, null);
   }
@@ -117,4 +150,11 @@ public final class ParsedReaderResultTestCase extends TestCase {
     assertEquals(type, result.getType());
   }
 
+  private static void doTestResult(byte[] rawBytes, ParsedReaderResultType type) {
+    Result fakeResult = new Result(null, rawBytes, null, null);
+    ParsedReaderResult result = ParsedReaderResult.parseReaderResult(fakeResult);
+    assertNotNull(result);
+    assertEquals(type, result.getType());
+  }
+
 }
\ No newline at end of file