1f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert/*
2f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * Copyright (C) 2010 The Android Open Source Project
3f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert *
4f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * Licensed under the Apache License, Version 2.0 (the "License");
5f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * you may not use this file except in compliance with the License.
6f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * You may obtain a copy of the License at
7f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert *
8f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert *      http://www.apache.org/licenses/LICENSE-2.0
9f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert *
10f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * Unless required by applicable law or agreed to in writing, software
11f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * distributed under the License is distributed on an "AS IS" BASIS,
12f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * See the License for the specific language governing permissions and
14f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * limitations under the License.
15f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert */
16f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
17f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringertpackage com.android.quicksearchbox;
18f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
19f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
20f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringertimport com.android.quicksearchbox.util.BarrierConsumer;
21f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
22f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringertimport android.content.Context;
23f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
24f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringertimport java.util.ArrayList;
25f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringertimport java.util.Collection;
2613ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringertimport java.util.List;
2772f9b08ce84d0e13daf2d1c112d4e6d1d3ada045Bjorn Bringertimport java.util.concurrent.Executor;
28f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
29f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert/**
30f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert * Base class for corpora backed by multiple sources.
31f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert */
32f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringertpublic abstract class MultiSourceCorpus extends AbstractCorpus {
33f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
3472f9b08ce84d0e13daf2d1c112d4e6d1d3ada045Bjorn Bringert    private final Executor mExecutor;
35f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
36f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    private final ArrayList<Source> mSources;
37f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
383cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    // calculated values based on properties of sources:
393cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    private boolean mSourcePropertiesValid;
403cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    private int mQueryThreshold;
413cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    private boolean mQueryAfterZeroResults;
423cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    private boolean mVoiceSearchEnabled;
43f3f70e5ae88f06ff6dabdec9e7c71a19ca1e7108Bjorn Bringert    private boolean mIncludeInAll;
443cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood
4596c7058210699c82445169048b7c0fdfb16f59eeBjorn Bringert    public MultiSourceCorpus(Context context, Config config,
4696c7058210699c82445169048b7c0fdfb16f59eeBjorn Bringert            Executor executor, Source... sources) {
4796c7058210699c82445169048b7c0fdfb16f59eeBjorn Bringert        super(context, config);
48f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        mExecutor = executor;
49f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
50f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        mSources = new ArrayList<Source>();
51f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        for (Source source : sources) {
52c9cdac4f63e717208539ff9be5a282a5eacd2429Mathew Inwood            addSource(source);
53c9cdac4f63e717208539ff9be5a282a5eacd2429Mathew Inwood        }
543cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood
55c9cdac4f63e717208539ff9be5a282a5eacd2429Mathew Inwood    }
56c9cdac4f63e717208539ff9be5a282a5eacd2429Mathew Inwood
57c9cdac4f63e717208539ff9be5a282a5eacd2429Mathew Inwood    protected void addSource(Source source) {
58c9cdac4f63e717208539ff9be5a282a5eacd2429Mathew Inwood        if (source != null) {
59c9cdac4f63e717208539ff9be5a282a5eacd2429Mathew Inwood            mSources.add(source);
603cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            // invalidate calculated values:
613cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            mSourcePropertiesValid = false;
62f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        }
63f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    }
64f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
65f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    public Collection<Source> getSources() {
66f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        return mSources;
67f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    }
68f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
69f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    /**
70f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * Creates a corpus result object for a set of source results.
71f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * This method should not call {@link Result#fill}.
72f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     *
73f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * @param query The query text.
74f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * @param results The results of the queries.
75f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert     * @param latency Latency in milliseconds of the suggestion queries.
76f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * @return An instance of {@link Result} or a subclass of it.
77f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     */
78f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert    protected Result createResult(String query, ArrayList<SourceResult> results, int latency) {
79f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        return new Result(query, results, latency);
8072f9b08ce84d0e13daf2d1c112d4e6d1d3ada045Bjorn Bringert    }
81f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
8213ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert    /**
83cd1e3ba5f7c3f5242345ff6f674281e3d6366e24Mathew Inwood     * Gets the sources to query for suggestions for the given input.
8413ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert     *
8513ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert     * @param query The current input.
86cd1e3ba5f7c3f5242345ff6f674281e3d6366e24Mathew Inwood     * @param onlyCorpus If true, this is the only corpus being queried.
8713ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert     * @return The sources to query.
8813ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert     */
89cd1e3ba5f7c3f5242345ff6f674281e3d6366e24Mathew Inwood    protected List<Source> getSourcesToQuery(String query, boolean onlyCorpus) {
903cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        List<Source> sources = new ArrayList<Source>();
913cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        for (Source candidate : getSources()) {
923cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            if (candidate.getQueryThreshold() <= query.length()) {
933cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood                sources.add(candidate);
943cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            }
953cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        }
963cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        return sources;
973cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    }
983cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood
9949fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert    private void updateSourceProperties() {
10049fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert        if (mSourcePropertiesValid) return;
1013cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        mQueryThreshold = Integer.MAX_VALUE;
1023cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        mQueryAfterZeroResults = false;
1033cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        mVoiceSearchEnabled = false;
104f3f70e5ae88f06ff6dabdec9e7c71a19ca1e7108Bjorn Bringert        mIncludeInAll = false;
1053cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        for (Source s : getSources()) {
1063cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            mQueryThreshold = Math.min(mQueryThreshold, s.getQueryThreshold());
1073cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            mQueryAfterZeroResults |= s.queryAfterZeroResults();
1083cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            mVoiceSearchEnabled |= s.voiceSearchEnabled();
109f3f70e5ae88f06ff6dabdec9e7c71a19ca1e7108Bjorn Bringert            mIncludeInAll |= s.includeInAll();
1103cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        }
1113cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        if (mQueryThreshold == Integer.MAX_VALUE) {
1123cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood            mQueryThreshold = 0;
1133cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        }
1143cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        mSourcePropertiesValid = true;
1153cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    }
1163cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood
1173cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    public int getQueryThreshold() {
11849fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert        updateSourceProperties();
1193cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        return mQueryThreshold;
1203cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    }
1213cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood
1223cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    public boolean queryAfterZeroResults() {
12349fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert        updateSourceProperties();
1243cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        return mQueryAfterZeroResults;
1253cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    }
1263cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood
1273cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood    public boolean voiceSearchEnabled() {
12849fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert        updateSourceProperties();
1293cb8178193a41f6c74ee396c318385a50dd624e1Mathew Inwood        return mVoiceSearchEnabled;
13013ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert    }
13113ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert
132f3f70e5ae88f06ff6dabdec9e7c71a19ca1e7108Bjorn Bringert    public boolean includeInAll() {
13349fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert        updateSourceProperties();
134f3f70e5ae88f06ff6dabdec9e7c71a19ca1e7108Bjorn Bringert        return mIncludeInAll;
13549fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert    }
13649fd8e0994577badc6194c2c3b5f771f2b793fe4Bjorn Bringert
137cd1e3ba5f7c3f5242345ff6f674281e3d6366e24Mathew Inwood    public CorpusResult getSuggestions(String query, int queryLimit, boolean onlyCorpus) {
138f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        LatencyTracker latencyTracker = new LatencyTracker();
139cd1e3ba5f7c3f5242345ff6f674281e3d6366e24Mathew Inwood        List<Source> sources = getSourcesToQuery(query, onlyCorpus);
140f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        BarrierConsumer<SourceResult> consumer =
14113ace1d0d0fa7e4c7aa7898a828763d8880db463Bjorn Bringert                new BarrierConsumer<SourceResult>(sources.size());
142cd1e3ba5f7c3f5242345ff6f674281e3d6366e24Mathew Inwood        boolean onlySource = sources.size() == 1;
14372f9b08ce84d0e13daf2d1c112d4e6d1d3ada045Bjorn Bringert        for (Source source : sources) {
14472f9b08ce84d0e13daf2d1c112d4e6d1d3ada045Bjorn Bringert            QueryTask<SourceResult> task = new QueryTask<SourceResult>(query, queryLimit,
145cd1e3ba5f7c3f5242345ff6f674281e3d6366e24Mathew Inwood                    source, null, consumer, onlySource);
14672f9b08ce84d0e13daf2d1c112d4e6d1d3ada045Bjorn Bringert            mExecutor.execute(task);
14772f9b08ce84d0e13daf2d1c112d4e6d1d3ada045Bjorn Bringert        }
148f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        ArrayList<SourceResult> results = consumer.getValues();
149f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        int latency = latencyTracker.getLatency();
150f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        Result result = createResult(query, results, latency);
151f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        result.fill();
152f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        return result;
153f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    }
154f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
155f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    /**
156f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * Base class for results returned by {@link MultiSourceCorpus#getSuggestions}.
157f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * Subclasses of {@link MultiSourceCorpus} should override
158f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * {@link MultiSourceCorpus#createResult} and return an instance of this class or a
159f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     * subclass.
160f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert     */
161f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    protected class Result extends ListSuggestionCursor implements CorpusResult {
162f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
163f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        private final ArrayList<SourceResult> mResults;
164f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
165f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        private final int mLatency;
166f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert
167f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        public Result(String userQuery, ArrayList<SourceResult> results, int latency) {
168f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            super(userQuery);
169f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            mResults = results;
170f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert            mLatency = latency;
171f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        }
172f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
173f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        protected ArrayList<SourceResult> getResults() {
174f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            return mResults;
175f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        }
176f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
177f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        /**
178f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert         * Fills the list of suggestions using the list of results.
179f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert         * The default implementation concatenates the results.
180f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert         */
181f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        public void fill() {
182f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            for (SourceResult result : getResults()) {
183f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert                int count = result.getCount();
184f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert                for (int i = 0; i < count; i++) {
185f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert                    result.moveTo(i);
186f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert                    add(new SuggestionPosition(result));
187f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert                }
188f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            }
189f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        }
190f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
191f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        public Corpus getCorpus() {
192f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            return MultiSourceCorpus.this;
193f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        }
194f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
195f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        public int getLatency() {
196f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert            return mLatency;
197f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert        }
198f95ce100dcbc77794b79b0187c566bb58b5978d3Bjorn Bringert
199f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        @Override
200f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        public void close() {
201f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            super.close();
202f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            for (SourceResult result : mResults) {
203f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert                result.close();
204f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert            }
205f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert        }
206bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert
207bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert        @Override
208bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert        public String toString() {
209848fa7a19abedc372452073abaf52780c7b6d78dAmith Yamasani            return "{" + getCorpus() + "[" + getUserQuery() + "]" + ";n=" + getCount() + "}";
210bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert        }
211f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert    }
212f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert
213f252dc7a25ba08b973ecc1cfbbce58eb78d42167Bjorn Bringert}
214