1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.phone.common.mail;
17 
18 import java.util.HashMap;
19 import java.util.Map;
20 
21 /**
22  * A utility class for creating and modifying Strings that are tagged and packed together.
23  *
24  * Uses non-printable (control chars) for internal delimiters;  Intended for regular displayable
25  * strings only, so please use base64 or other encoding if you need to hide any binary data here.
26  *
27  * Binary compatible with Address.pack() format, which should migrate to use this code.
28  */
29 public class PackedString {
30 
31     /**
32      * Packing format is:
33      *   element : [ value ] or [ value TAG-DELIMITER tag ]
34      *   packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]*
35      */
36     private static final char DELIMITER_ELEMENT = '\1';
37     private static final char DELIMITER_TAG = '\2';
38 
39     private String mString;
40     private HashMap<String, String> mExploded;
41     private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>();
42 
43     /**
44      * Create a packed string using an already-packed string (e.g. from database)
45      * @param string packed string
46      */
PackedString(String string)47     public PackedString(String string) {
48         mString = string;
49         mExploded = null;
50     }
51 
52     /**
53      * Get the value referred to by a given tag.  If the tag does not exist, return null.
54      * @param tag identifier of string of interest
55      * @return returns value, or null if no string is found
56      */
get(String tag)57     public String get(String tag) {
58         if (mExploded == null) {
59             mExploded = explode(mString);
60         }
61         return mExploded.get(tag);
62     }
63 
64     /**
65      * Return a map of all of the values referred to by a given tag.  This is a shallow
66      * copy, don't edit the values.
67      * @return a map of the values in the packed string
68      */
unpack()69     public Map<String, String> unpack() {
70         if (mExploded == null) {
71             mExploded = explode(mString);
72         }
73         return new HashMap<String,String>(mExploded);
74     }
75 
76     /**
77      * Read out all values into a map.
78      */
explode(String packed)79     private static HashMap<String, String> explode(String packed) {
80         if (packed == null || packed.length() == 0) {
81             return EMPTY_MAP;
82         }
83         HashMap<String, String> map = new HashMap<String, String>();
84 
85         int length = packed.length();
86         int elementStartIndex = 0;
87         int elementEndIndex = 0;
88         int tagEndIndex = packed.indexOf(DELIMITER_TAG);
89 
90         while (elementStartIndex < length) {
91             elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
92             if (elementEndIndex == -1) {
93                 elementEndIndex = length;
94             }
95             String tag;
96             String value;
97             if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
98                 // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
99                 // so synthesize a positional tag for the value, and don't update tagEndIndex
100                 value = packed.substring(elementStartIndex, elementEndIndex);
101                 tag = Integer.toString(map.size());
102             } else {
103                 value = packed.substring(elementStartIndex, tagEndIndex);
104                 tag = packed.substring(tagEndIndex + 1, elementEndIndex);
105                 // scan forward for next tag, if any
106                 tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
107             }
108             map.put(tag, value);
109             elementStartIndex = elementEndIndex + 1;
110         }
111 
112         return map;
113     }
114 
115     /**
116      * Builder class for creating PackedString values.  Can also be used for editing existing
117      * PackedString representations.
118      */
119     static public class Builder {
120         HashMap<String, String> mMap;
121 
122         /**
123          * Create a builder that's empty (for filling)
124          */
Builder()125         public Builder() {
126             mMap = new HashMap<String, String>();
127         }
128 
129         /**
130          * Create a builder using the values of an existing PackedString (for editing).
131          */
Builder(String packed)132         public Builder(String packed) {
133             mMap = explode(packed);
134         }
135 
136         /**
137          * Add a tagged value
138          * @param tag identifier of string of interest
139          * @param value the value to record in this position.  null to delete entry.
140          */
put(String tag, String value)141         public void put(String tag, String value) {
142             if (value == null) {
143                 mMap.remove(tag);
144             } else {
145                 mMap.put(tag, value);
146             }
147         }
148 
149         /**
150          * Get the value referred to by a given tag.  If the tag does not exist, return null.
151          * @param tag identifier of string of interest
152          * @return returns value, or null if no string is found
153          */
get(String tag)154         public String get(String tag) {
155             return mMap.get(tag);
156         }
157 
158         /**
159          * Pack the values and return a single, encoded string
160          */
161         @Override
toString()162         public String toString() {
163             StringBuilder sb = new StringBuilder();
164             for (Map.Entry<String,String> entry : mMap.entrySet()) {
165                 if (sb.length() > 0) {
166                     sb.append(DELIMITER_ELEMENT);
167                 }
168                 sb.append(entry.getValue());
169                 sb.append(DELIMITER_TAG);
170                 sb.append(entry.getKey());
171             }
172             return sb.toString();
173         }
174     }
175 }
176