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 */
16package com.android.phone.common.mail;
17
18import java.util.HashMap;
19import 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 */
29public 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     */
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     */
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     */
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     */
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         */
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         */
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         */
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         */
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
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