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