1b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu/*
2b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * Copyright (C) 2016 The Android Open Source Project
3b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu *
4b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * Licensed under the Apache License, Version 2.0 (the "License");
5b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * you may not use this file except in compliance with the License.
6b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * You may obtain a copy of the License at
7b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu *
8b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu *      http://www.apache.org/licenses/LICENSE-2.0
9b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu *
10b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * Unless required by applicable law or agreed to in writing, software
11b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * distributed under the License is distributed on an "AS IS" BASIS,
12b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * See the License for the specific language governing permissions and
14b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * limitations under the License.
15b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu */
16b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu
17b378283f8783e0e20e70a3cbbcdcb3e07b434493Peter Qiupackage com.android.server.wifi.hotspot2;
1831891afce40b903ada9b24ec12e3648ae6aa27b2Jan Nordqvist
19b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiuimport android.text.TextUtils;
20b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu
2171a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvistimport com.android.server.wifi.hotspot2.Utils;
2271a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist
236a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvistimport java.util.HashMap;
246a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvistimport java.util.Iterator;
256a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvistimport java.util.List;
266a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvistimport java.util.Map;
276a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
28b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu/**
29b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu * Utility class for matching domain names.
30b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu */
316a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvistpublic class DomainMatcher {
32b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    public static final int MATCH_NONE = 0;
33b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    public static final int MATCH_PRIMARY = 1;
34b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    public static final int MATCH_SECONDARY = 2;
356a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
36b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    /**
37b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * The root of the Label tree.
38b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     */
396a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist    private final Label mRoot;
406a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
41b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    /**
42b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * Label tree representation for the domain name.  Labels are delimited by "." in the domain
43b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * name.
44b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     *
45b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * For example, the tree representation of "android.google.com" as a primary domain:
46b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * [com, None] -> [google, None] -> [android, Primary]
47b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     *
48b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     */
496a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist    private static class Label {
5071a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist        private final Map<String, Label> mSubDomains;
51b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        private int mMatch;
526a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
53b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        Label(int match) {
546a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist            mMatch = match;
55b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            mSubDomains = new HashMap<String, Label>();
566a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist        }
576a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
58b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        /**
59b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * Add sub-domains to this label.
60b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         *
61b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * @param labels The iterator of domain label strings
62b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * @param match The match status of the domain
63b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         */
64b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        public void addDomain(Iterator<String> labels, int match) {
656a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist            String labelName = labels.next();
66b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            // Create the Label object if it doesn't exist yet.
67b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            Label subLabel = mSubDomains.get(labelName);
68b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            if (subLabel == null) {
69b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                subLabel = new Label(MATCH_NONE);
7071a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                mSubDomains.put(labelName, subLabel);
71b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            }
72b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu
73b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            if (labels.hasNext()) {
74b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                // Adding sub-domain.
7571a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                subLabel.addDomain(labels, match);
7671a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            } else {
77b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                // End of the domain, update the match status.
78b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                subLabel.mMatch = match;
796a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist            }
806a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist        }
816a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
82b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        /**
83b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * Return the Label for the give label string.
84b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * @param labelString The label string to look for
85b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * @return {@link Label}
86b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         */
87b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        public Label getSubLabel(String labelString) {
8871a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            return mSubDomains.get(labelString);
896a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist        }
906a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
91b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        /**
92b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * Return the match status
93b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         *
94b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         * @return The match status
95b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu         */
96b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        public int getMatch() {
976a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist            return mMatch;
986a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist        }
9971a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist
10071a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist        private void toString(StringBuilder sb) {
10171a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            if (mSubDomains != null) {
10271a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                sb.append(".{");
10371a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                for (Map.Entry<String, Label> entry : mSubDomains.entrySet()) {
10471a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                    sb.append(entry.getKey());
10571a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                    entry.getValue().toString(sb);
10671a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                }
10771a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                sb.append('}');
10871a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            } else {
10971a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist                sb.append('=').append(mMatch);
11071a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            }
11171a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist        }
11271a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist
11371a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist        @Override
11471a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist        public String toString() {
11571a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            StringBuilder sb = new StringBuilder();
11671a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            toString(sb);
11771a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            return sb.toString();
11871a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist        }
1196a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist    }
1206a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
121b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    public DomainMatcher(String primaryDomain, List<String> secondaryDomains) {
122b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        // Create the root label.
123b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        mRoot = new Label(MATCH_NONE);
124b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu
125b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        // Add secondary domains.
126b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        if (secondaryDomains != null) {
127b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            for (String domain : secondaryDomains) {
128b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                if (!TextUtils.isEmpty(domain)) {
129b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                    List<String> secondaryLabel = Utils.splitDomain(domain);
130b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                    mRoot.addDomain(secondaryLabel.iterator(), MATCH_SECONDARY);
131b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                }
132b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            }
133b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        }
134b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu
135b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        // Add primary domain, primary overwrites secondary.
136b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        if (!TextUtils.isEmpty(primaryDomain)) {
137b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            List<String> primaryLabel = Utils.splitDomain(primaryDomain);
138b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            mRoot.addDomain(primaryLabel.iterator(), MATCH_PRIMARY);
1396a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist        }
1406a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist    }
1416a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
14277f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist    /**
143b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * Check if domain is either the same or a sub-domain of any of the domains in the
144b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * domain tree in this matcher, i.e. all or a sub-set of the labels in domain matches
145b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * a path in the tree.
146b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     *
147b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * This will have precedence for matching primary domain over secondary domain if both
148b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * are found.
149b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     *
150b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * For example, with primary domain set to "test.google.com" and secondary domain set to
151b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * "google.com":
152b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * "test2.test.google.com" -> Match.Primary
153b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * "test1.google.com" -> Match.Secondary
154b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     *
155b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * @param domainName Domain name to be checked.
156b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * @return The match status
15777f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist     */
158b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    public int isSubDomain(String domainName) {
159b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        if (TextUtils.isEmpty(domainName)) {
160b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            return MATCH_NONE;
161b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        }
162b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        List<String> domainLabels = Utils.splitDomain(domainName);
1636a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist
1646a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist        Label label = mRoot;
165b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        int match = MATCH_NONE;
166b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        for (String labelString : domainLabels) {
16771a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            label = label.getSubLabel(labelString);
16871a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist            if (label == null) {
169b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                break;
170b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            } else if (label.getMatch() != MATCH_NONE) {
171b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                match = label.getMatch();
172b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                if (match == MATCH_PRIMARY) {
173b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                    break;
174b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu                }
1756a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist            }
1766a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist        }
177b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        return match;
1786a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist    }
17971a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist
180b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    /**
181b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * Check if domain2 is a sub-domain of domain1.
182b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     *
183b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * @param domain1 The string of the first domain
184b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * @param domain2 The string of the second domain
185b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     * @return true if the second domain is the sub-domain of the first
186b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu     */
187b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu    public static boolean arg2SubdomainOfArg1(String domain1, String domain2) {
188b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        if (TextUtils.isEmpty(domain1) || TextUtils.isEmpty(domain2)) {
189b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            return false;
190b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        }
191b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu
192b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        List<String> labels1 = Utils.splitDomain(domain1);
193b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        List<String> labels2 = Utils.splitDomain(domain2);
194b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu
195b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        // domain2 must be the same or longer than domain1 in order to be a sub-domain.
196b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        if (labels2.size() < labels1.size()) {
19777f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist            return false;
19877f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist        }
19977f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist
200b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        Iterator<String> l1 = labels1.iterator();
201b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu        Iterator<String> l2 = labels2.iterator();
20277f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist
20377f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist        while(l1.hasNext()) {
204b40ba9e6ef82ac6c82869d1b562701483b8f1fc2Peter Qiu            if (!TextUtils.equals(l1.next(), l2.next())) {
20577f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist                return false;
20677f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist            }
20777f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist        }
20877f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist        return true;
20977f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist    }
21077f2b82a2e80af8da52c22d69a76def6d4209757Jan Nordqvist
21171a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist    @Override
21271a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist    public String toString() {
21371a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist        return "Domain matcher " + mRoot;
21471a988c8e9859244b83cd55bb6b6ee913fcaf95cJan Nordqvist    }
2156a3903fed590e369b576bddbe1ae2d788768ddfeJan Nordqvist}
216