VendorConfig.java revision b87c08da82d50b1358f068a3ae44068022c7af2e
1/*
2 * Copyright (C) 2016 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.printservice.recommendation.plugin.mdnsFilter;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.content.res.XmlResourceParser;
23import android.util.ArrayMap;
24import com.android.internal.annotations.Immutable;
25import com.android.internal.util.Preconditions;
26import com.android.printservice.recommendation.R;
27import org.xmlpull.v1.XmlPullParser;
28import org.xmlpull.v1.XmlPullParserException;
29
30import java.io.IOException;
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.Collections;
34import java.util.List;
35
36/**
37 * Vendor configuration as read from {@link R.xml#vendorconfigs vendorconfigs.xml}. Configuration
38 * can be read via {@link #getConfig(Context, String)}.
39 */
40@Immutable
41public class VendorConfig {
42    /** Lock for {@link #sConfigs} */
43    private static final Object sLock = new Object();
44
45    /** Strings used as XML tags */
46    private static final String VENDORS_TAG = "vendors";
47    private static final String VENDOR_TAG = "vendor";
48    private static final String NAME_TAG = "name";
49    private static final String PACKAGE_TAG = "package";
50    private static final String MDNSNAMES_TAG = "mdns-names";
51    private static final String MDNSNAME_TAG = "mdns-name";
52
53    /** Map from vendor name to config. Initialized on first {@link #getConfig use}. */
54    private static @Nullable ArrayMap<String, VendorConfig> sConfigs;
55
56    /** Localized vendor name */
57    public final @NonNull String name;
58
59    /** Package name containing the print service for this vendor */
60    public final @NonNull String packageName;
61
62    /** mDNS names used by this vendor */
63    public final @NonNull List<String> mDNSNames;
64
65    /**
66     * Create an immutable configuration.
67     */
68    private VendorConfig(@NonNull String name, @NonNull String packageName,
69            @NonNull List<String> mDNSNames) {
70        this.name = Preconditions.checkStringNotEmpty(name);
71        this.packageName = Preconditions.checkStringNotEmpty(packageName);
72        this.mDNSNames = Preconditions.checkCollectionElementsNotNull(mDNSNames, "mDNSName");
73    }
74
75    /**
76     * Get the configuration for a vendor.
77     *
78     * @param context Calling context
79     * @param name    The name of the config to read
80     *
81     * @return the config for the vendor or null if not found
82     *
83     * @throws IOException
84     * @throws XmlPullParserException
85     */
86    public static @Nullable VendorConfig getConfig(@NonNull Context context, @NonNull String name)
87            throws IOException, XmlPullParserException {
88        synchronized (sLock) {
89            if (sConfigs == null) {
90                sConfigs = readVendorConfigs(context);
91            }
92
93            return sConfigs.get(name);
94        }
95    }
96
97    /**
98     * Get all known vendor configurations.
99     *
100     * @param context Calling context
101     *
102     * @return The known configurations
103     *
104     * @throws IOException
105     * @throws XmlPullParserException
106     */
107    public static @NonNull Collection<VendorConfig> getAllConfigs(@NonNull Context context)
108            throws IOException, XmlPullParserException {
109        synchronized (sLock) {
110            if (sConfigs == null) {
111                sConfigs = readVendorConfigs(context);
112            }
113
114            return sConfigs.values();
115        }
116    }
117
118    /**
119     * Read the text from a XML tag.
120     *
121     * @param parser XML parser to read from
122     *
123     * @return The text or "" if no text was found
124     *
125     * @throws IOException
126     * @throws XmlPullParserException
127     */
128    private static @NonNull String readText(XmlPullParser parser)
129            throws IOException, XmlPullParserException {
130        String result = "";
131
132        if (parser.next() == XmlPullParser.TEXT) {
133            result = parser.getText();
134            parser.nextTag();
135        }
136
137        return result;
138    }
139
140    /**
141     * Read a tag with a text content from the parser.
142     *
143     * @param parser  XML parser to read from
144     * @param tagName The name of the tag to read
145     *
146     * @return The text content of the tag
147     *
148     * @throws IOException
149     * @throws XmlPullParserException
150     */
151    private static @NonNull String readSimpleTag(@NonNull Context context,
152            @NonNull XmlPullParser parser, @NonNull String tagName, boolean resolveReferences)
153            throws IOException, XmlPullParserException {
154        parser.require(XmlPullParser.START_TAG, null, tagName);
155        String text = readText(parser);
156        parser.require(XmlPullParser.END_TAG, null, tagName);
157
158        if (resolveReferences && text.startsWith("@")) {
159            return context.getResources().getString(
160                    context.getResources().getIdentifier(text, null, context.getPackageName()));
161        } else {
162            return text;
163        }
164    }
165
166    /**
167     * Read content of a list of tags.
168     *
169     * @param parser     XML parser to read from
170     * @param tagName    The name of the list tag
171     * @param subTagName The name of the list-element tags
172     * @param tagReader  The {@link TagReader reader} to use to read the tag content
173     * @param <T>        The type of the parsed tag content
174     *
175     * @return A list of {@link T}
176     *
177     * @throws XmlPullParserException
178     * @throws IOException
179     */
180    private static @NonNull <T> ArrayList<T> readTagList(@NonNull XmlPullParser parser,
181            @NonNull String tagName, @NonNull String subTagName, @NonNull TagReader<T> tagReader)
182            throws XmlPullParserException, IOException {
183        ArrayList<T> entries = new ArrayList<>();
184
185        parser.require(XmlPullParser.START_TAG, null, tagName);
186        while (parser.next() != XmlPullParser.END_TAG) {
187            if (parser.getEventType() != XmlPullParser.START_TAG) {
188                continue;
189            }
190
191            if (parser.getName().equals(subTagName)) {
192                entries.add(tagReader.readTag(parser, subTagName));
193            } else {
194                throw new XmlPullParserException(
195                        "Unexpected subtag of " + tagName + ": " + parser.getName());
196            }
197        }
198
199        return entries;
200    }
201
202    /**
203     * Read the vendor configuration file.
204     *
205     * @param context The content issuing the read
206     *
207     * @return An map pointing from vendor name to config
208     *
209     * @throws IOException
210     * @throws XmlPullParserException
211     */
212    private static @NonNull ArrayMap<String, VendorConfig> readVendorConfigs(
213            @NonNull final Context context) throws IOException, XmlPullParserException {
214        try (XmlResourceParser parser = context.getResources().getXml(R.xml.vendorconfigs)) {
215            // Skip header
216            int parsingEvent;
217            do {
218                parsingEvent = parser.next();
219            } while (parsingEvent != XmlResourceParser.START_TAG);
220
221            ArrayList<VendorConfig> configs = readTagList(parser, VENDORS_TAG, VENDOR_TAG,
222                    new TagReader<VendorConfig>() {
223                        public VendorConfig readTag(XmlPullParser parser, String tagName)
224                                throws XmlPullParserException, IOException {
225                            return readVendorConfig(context, parser, tagName);
226                        }
227                    });
228
229            ArrayMap<String, VendorConfig> configMap = new ArrayMap<>(configs.size());
230            final int numConfigs = configs.size();
231            for (int i = 0; i < numConfigs; i++) {
232                VendorConfig config = configs.get(i);
233
234                configMap.put(config.name, config);
235            }
236
237            return configMap;
238        }
239    }
240
241    /**
242     * Read a single vendor configuration.
243     *
244     * @param parser  XML parser to read from
245     * @param tagName The vendor tag
246     * @param context Calling context
247     *
248     * @return A config
249     *
250     * @throws XmlPullParserException
251     * @throws IOException
252     */
253    private static VendorConfig readVendorConfig(@NonNull final Context context,
254            @NonNull XmlPullParser parser, @NonNull String tagName) throws XmlPullParserException,
255            IOException {
256        parser.require(XmlPullParser.START_TAG, null, tagName);
257
258        String name = null;
259        String packageName = null;
260        List<String> mDNSNames = null;
261
262        while (parser.next() != XmlPullParser.END_TAG) {
263            if (parser.getEventType() != XmlPullParser.START_TAG) {
264                continue;
265            }
266
267            String subTagName = parser.getName();
268
269            switch (subTagName) {
270                case NAME_TAG:
271                    name = readSimpleTag(context, parser, NAME_TAG, false);
272                    break;
273                case PACKAGE_TAG:
274                    packageName = readSimpleTag(context, parser, PACKAGE_TAG, true);
275                    break;
276                case MDNSNAMES_TAG:
277                    mDNSNames = readTagList(parser, MDNSNAMES_TAG, MDNSNAME_TAG,
278                            new TagReader<String>() {
279                                public String readTag(XmlPullParser parser, String tagName)
280                                        throws XmlPullParserException, IOException {
281                                    return readSimpleTag(context, parser, tagName, true);
282                                }
283                            }
284                    );
285                    break;
286                default:
287                    throw new XmlPullParserException("Unexpected subtag of " + tagName + ": "
288                            + subTagName);
289
290            }
291        }
292
293        if (name == null) {
294            throw new XmlPullParserException("name is required");
295        }
296
297        if (packageName == null) {
298            throw new XmlPullParserException("package is required");
299        }
300
301        if (mDNSNames == null) {
302            mDNSNames = Collections.emptyList();
303        }
304
305        // A vendor config should be immutable
306        mDNSNames = Collections.unmodifiableList(mDNSNames);
307
308        return new VendorConfig(name, packageName, mDNSNames);
309    }
310
311    @Override
312    public String toString() {
313        return name + " -> " + packageName + ", " + mDNSNames;
314    }
315
316    /**
317     * Used a a "function pointer" when reading a tag in {@link #readTagList(XmlPullParser, String,
318     * String, TagReader)}.
319     *
320     * @param <T> The type of content to read
321     */
322    private interface TagReader<T> {
323        T readTag(XmlPullParser parser, String tagName) throws XmlPullParserException, IOException;
324    }
325}
326