org.tomahawk.libtomahawk.resolver.PipeLine.java Source code

Java tutorial

Introduction

Here is the source code for org.tomahawk.libtomahawk.resolver.PipeLine.java

Source

/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
 *
 *   Copyright 2013, Enno Gottschalk <mrmaffen@googlemail.com>
 *
 *   Tomahawk is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   Tomahawk is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 */
package org.tomahawk.libtomahawk.resolver;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;

import org.tomahawk.libtomahawk.infosystem.InfoSystemUtils;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverMetaData;
import org.tomahawk.libtomahawk.resolver.models.ScriptResolverUrlResult;
import org.tomahawk.libtomahawk.resolver.spotify.SpotifyResolver;
import org.tomahawk.libtomahawk.utils.TomahawkUtils;
import org.tomahawk.tomahawk_android.R;
import org.tomahawk.tomahawk_android.TomahawkApp;
import org.tomahawk.tomahawk_android.utils.ThreadManager;
import org.tomahawk.tomahawk_android.utils.TomahawkRunnable;

import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The {@link PipeLine} is being used to provide all the resolving functionality. All {@link
 * Resolver}s are stored and invoked here. Callbacks which report the found {@link Result}s are also
 * included in this class.
 */
public class PipeLine implements Resolver.OnResolverReadyListener {

    private final static String TAG = PipeLine.class.getSimpleName();

    public static final int PIPELINE_SEARCHTYPE_TRACKS = 0;

    public static final int PIPELINE_SEARCHTYPE_ARTISTS = 1;

    public static final int PIPELINE_SEARCHTYPE_ALBUMS = 2;

    public static final String URL_TYPE_TRACK = "track";

    public static final String URL_TYPE_ALBUM = "album";

    public static final String URL_TYPE_ARTIST = "artist";

    public static final String URL_TYPE_PLAYLIST = "playlist";

    public static final String PIPELINE_RESULTSREPORTED = "org.tomahawk.tomahawk_android.pipeline_resultsreported";

    public static final String PIPELINE_STREAMURLREPORTED = "org.tomahawk.tomahawk_android.pipeline_streamurlreported";

    public static final String PIPELINE_STREAMURLREPORTED_URL = "org.tomahawk.tomahawk_android.pipeline_streamurlreported_url";

    public static final String PIPELINE_STREAMURLREPORTED_RESULTKEY = "org.tomahawk.tomahawk_android.pipeline_streamurlreported_resultkey";

    public static final String PIPELINE_RESULTSREPORTED_QUERYKEY = "org.tomahawk.tomahawk_android.pipeline_resultsreported_querykey";

    public static final String PIPELINE_URLLOOKUPFINISHED = "org.tomahawk.tomahawk_android.pipeline_urllookupfinished";

    public static final String PIPELINE_URLLOOKUPFINISHED_URL = "org.tomahawk.tomahawk_android.pipeline_urllookupfinished_url";

    public static final String PIPELINE_URLLOOKUPFINISHED_RESOLVERID = "org.tomahawk.tomahawk_android.pipeline_urllookupfinished_resolverid";

    private static final float MINSCORE = 0.5F;

    private static class Holder {

        private static final PipeLine instance = new PipeLine();

    }

    private ArrayList<Resolver> mResolvers = new ArrayList<Resolver>();

    private ConcurrentHashMap<String, Query> mWaitingQueries = new ConcurrentHashMap<String, Query>();

    private HashSet<String> mWaitingUrlLookups = new HashSet<String>();

    private boolean mAllResolversAdded;

    private ConcurrentHashMap<String, ResolverUrlHandler> mUrlHandlerMap = new ConcurrentHashMap<String, ResolverUrlHandler>();

    private ConcurrentHashMap<String, ScriptResolverUrlResult> mUrlLookupMap = new ConcurrentHashMap<String, ScriptResolverUrlResult>();

    private PipeLine() {
        try {
            String[] plugins = TomahawkApp.getContext().getAssets().list("js/resolvers");
            for (String plugin : plugins) {
                String path = "js/resolvers/" + plugin + "/content";
                try {
                    String rawJsonString = TomahawkUtils.inputStreamToString(
                            TomahawkApp.getContext().getAssets().open(path + "/metadata.json"));
                    ScriptResolverMetaData metaData = InfoSystemUtils.getObjectMapper().readValue(rawJsonString,
                            ScriptResolverMetaData.class);
                    ScriptResolver scriptResolver = new ScriptResolver(metaData, path, this);
                    mResolvers.add(scriptResolver);
                } catch (FileNotFoundException e) {
                    Log.e(TAG, "PipeLine: " + e.getClass() + ": " + e.getLocalizedMessage());
                } catch (JsonMappingException e) {
                    Log.e(TAG, "PipeLine: " + e.getClass() + ": " + e.getLocalizedMessage());
                } catch (JsonParseException e) {
                    Log.e(TAG, "PipeLine: " + e.getClass() + ": " + e.getLocalizedMessage());
                } catch (IOException e) {
                    Log.e(TAG, "PipeLine: " + e.getClass() + ": " + e.getLocalizedMessage());
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "ensureInit: " + e.getClass() + ": " + e.getLocalizedMessage());
        }
        mResolvers.add(new DataBaseResolver(
                TomahawkApp.getContext().getString(R.string.local_collection_pretty_name), this));
        SpotifyResolver spotifyResolver = new SpotifyResolver(this);
        mResolvers.add(spotifyResolver);
        setAllResolversAdded(true);
    }

    public static PipeLine getInstance() {
        return Holder.instance;
    }

    /**
     * Get the {@link Resolver} with the given id, null if not found
     */
    public Resolver getResolver(String id) {
        for (Resolver resolver : mResolvers) {
            if (resolver.getId().equals(id)) {
                return resolver;
            }
        }
        return null;
    }

    /**
     * Get the ArrayList of all {@link org.tomahawk.libtomahawk.resolver.ScriptResolver}s
     */
    public ArrayList<ScriptResolver> getScriptResolvers() {
        ArrayList<ScriptResolver> scriptResolvers = new ArrayList<ScriptResolver>();
        for (Resolver resolver : mResolvers) {
            if (resolver instanceof ScriptResolver) {
                scriptResolvers.add((ScriptResolver) resolver);
            }
        }
        return scriptResolvers;
    }

    /**
     * This will invoke every {@link Resolver} to resolve the given fullTextQuery. If there already
     * is a {@link Query} with the same fullTextQuery, the old resultList will be reported.
     */
    public String resolve(String fullTextQuery) {
        return resolve(fullTextQuery, false);
    }

    /**
     * This will invoke every {@link Resolver} to resolve the given fullTextQuery. If there already
     * is a {@link Query} with the same fullTextQuery, the old resultList will be reported.
     */
    public String resolve(String fullTextQuery, boolean forceOnlyLocal) {
        if (fullTextQuery != null && !TextUtils.isEmpty(fullTextQuery)) {
            Query q = Query.get(fullTextQuery, forceOnlyLocal);
            resolve(q, forceOnlyLocal);
            return q.getCacheKey();
        }
        return null;
    }

    /**
     * This will invoke every {@link Resolver} to resolve the given {@link
     * org.tomahawk.libtomahawk.collection.Track}/{@link org.tomahawk.libtomahawk.collection.Artist}/{@link
     * org.tomahawk.libtomahawk.collection.Album}. If there already is a {@link Query} with the same
     * {@link org.tomahawk.libtomahawk.collection.Track}/{@link org.tomahawk.libtomahawk.collection.Artist}/{@link
     * org.tomahawk.libtomahawk.collection.Album}, the old resultList will be reported.
     */
    public String resolve(String trackName, String albumName, String artistName) {
        if (trackName != null && !TextUtils.isEmpty(trackName)) {
            Query q = Query.get(trackName, albumName, artistName, false);
            return resolve(q);
        }
        return null;
    }

    /**
     * This will invoke every {@link Resolver} to resolve the given {@link Query}.
     */
    public String resolve(Query q) {
        return resolve(q, false);
    }

    /**
     * This will invoke every {@link Resolver} to resolve the given {@link Query}.
     */
    public String resolve(final Query q, final boolean forceOnlyLocal) {
        final TomahawkRunnable r = new TomahawkRunnable(TomahawkRunnable.PRIORITY_IS_RESOLVING) {
            @Override
            public void run() {
                if (!forceOnlyLocal && q.isSolved()) {
                    sendResultsReportBroadcast(q.getCacheKey());
                } else {
                    if (!isEveryResolverReady()) {
                        if (!mWaitingQueries.containsKey(q.getCacheKey())) {
                            mWaitingQueries.put(q.getCacheKey(), q);
                        }
                    } else {
                        for (final Resolver resolver : mResolvers) {
                            if (shouldResolve(resolver, q, forceOnlyLocal)) {
                                resolver.resolve(q);
                            }
                        }
                    }
                }
            }
        };
        ThreadManager.getInstance().execute(r, q);
        return q.getCacheKey();
    }

    /**
     * Method to determine if a given Resolver should resolve the query or not
     */
    public boolean shouldResolve(Resolver resolver, Query q, boolean forceOnlyLocal) {
        if (!forceOnlyLocal && !q.isOnlyLocal()) {
            if (resolver instanceof ScriptResolver) {
                ScriptResolver scriptResolver = ((ScriptResolver) resolver);
                return scriptResolver.isEnabled();
            } else {
                return true;
            }
        } else if (resolver instanceof DataBaseResolver) {
            return true;
        }
        return false;
    }

    /**
     * Resolve the given ArrayList of {@link org.tomahawk.libtomahawk.resolver.Query}s and return a
     * HashSet containing all query ids
     */
    public HashSet<String> resolve(ArrayList<Query> queries) {
        return resolve(queries, false);
    }

    /**
     * Resolve the given ArrayList of {@link org.tomahawk.libtomahawk.resolver.Query}s and return a
     * HashSet containing all query keys
     */
    public HashSet<String> resolve(ArrayList<Query> queries, boolean forceOnlyLocal) {
        HashSet<String> queryKeys = new HashSet<String>();
        if (queries != null) {
            for (Query query : queries) {
                if (forceOnlyLocal || !query.isSolved()) {
                    queryKeys.add(resolve(query, forceOnlyLocal));
                }
            }
        }
        return queryKeys;
    }

    /**
     * Send a broadcast containing the stream url which has previously been requested by a
     * MediaPlayerInterface.
     */
    public void sendStreamUrlReportBroadcast(final String resultKey, final String url,
            final Map<String, String> headers) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    String finalUrl = url;
                    if (headers != null) {
                        finalUrl = TomahawkUtils.getRedirectedUrl(TomahawkUtils.HTTP_METHOD_GET, url, headers);
                    }
                    Intent reportIntent = new Intent(PIPELINE_STREAMURLREPORTED);
                    reportIntent.putExtra(PIPELINE_STREAMURLREPORTED_RESULTKEY, resultKey);
                    reportIntent.putExtra(PIPELINE_STREAMURLREPORTED_URL, finalUrl);
                    TomahawkApp.getContext().sendBroadcast(reportIntent);
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (KeyManagementException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * Send a broadcast containing the key of the resolved {@link Query}.
     */
    private void sendResultsReportBroadcast(String queryKey) {
        Intent reportIntent = new Intent(PIPELINE_RESULTSREPORTED);
        reportIntent.putExtra(PIPELINE_RESULTSREPORTED_QUERYKEY, queryKey);
        TomahawkApp.getContext().sendBroadcast(reportIntent);
    }

    /**
     * If the {@link ScriptResolver} has resolved the {@link Query}, this method will be called.
     * This method will then calculate a score and assign it to every {@link Result}. If the score
     * is higher than MINSCORE the {@link Result} is added to the output resultList.
     *
     * @param queryKey the {@link Query}'s key
     * @param results  the unfiltered {@link ArrayList} of {@link Result}s
     */
    public void reportResults(final String queryKey, final ArrayList<Result> results, final String resolverId) {
        int priority;
        if (TomahawkApp.PLUGINNAME_USERCOLLECTION.equals(resolverId)) {
            priority = TomahawkRunnable.PRIORITY_IS_REPORTING_LOCALSOURCE;
        } else if (TomahawkApp.PLUGINNAME_SPOTIFY.equals(resolverId)
                || TomahawkApp.PLUGINNAME_DEEZER.equals(resolverId)
                || TomahawkApp.PLUGINNAME_BEATSMUSIC.equals(resolverId)
                || TomahawkApp.PLUGINNAME_RDIO.equals(resolverId)) {
            priority = TomahawkRunnable.PRIORITY_IS_REPORTING_SUBSCRIPTION;
        } else {
            priority = TomahawkRunnable.PRIORITY_IS_REPORTING;
        }
        ThreadManager.getInstance().execute(new TomahawkRunnable(priority) {
            @Override
            public void run() {
                ArrayList<Result> cleanTrackResults = new ArrayList<Result>();
                ArrayList<Result> cleanAlbumResults = new ArrayList<Result>();
                ArrayList<Result> cleanArtistResults = new ArrayList<Result>();
                Query q = Query.getQueryByKey(queryKey);
                if (q != null) {
                    for (Result r : results) {
                        if (r != null) {
                            r.setTrackScore(q.howSimilar(r, PIPELINE_SEARCHTYPE_TRACKS));
                            if (r.getTrackScore() >= MINSCORE && !cleanTrackResults.contains(r)) {
                                r.setType(Result.RESULT_TYPE_TRACK);
                                cleanTrackResults.add(r);
                            }
                            if (q.isFullTextQuery()) {
                                r.setAlbumScore(q.howSimilar(r, PIPELINE_SEARCHTYPE_ALBUMS));
                                if (r.getAlbumScore() >= MINSCORE && !cleanAlbumResults.contains(r)) {
                                    r.setType(Result.RESULT_TYPE_ALBUM);
                                    cleanAlbumResults.add(r);
                                }
                                r.setArtistScore(q.howSimilar(r, PIPELINE_SEARCHTYPE_ARTISTS));
                                if (r.getArtistScore() >= MINSCORE && !cleanArtistResults.contains(r)) {
                                    r.setType(Result.RESULT_TYPE_ARTIST);
                                    cleanArtistResults.add(r);
                                }
                            }
                        }
                    }
                    q.addArtistResults(cleanArtistResults);
                    q.addAlbumResults(cleanAlbumResults);
                    q.addTrackResults(cleanTrackResults);
                    sendResultsReportBroadcast(q.getCacheKey());
                    if (q.isSolved()) {
                        ThreadManager.getInstance().stop(q);
                    }
                }
            }
        });
    }

    public void lookupUrl(String url) {
        if (!isEveryResolverReady()) {
            Log.d(TAG, "lookupUrl - enqueuing url: " + url);
            mWaitingUrlLookups.add(url);
        } else {
            Log.d(TAG, "lookupUrl - looking up url: " + url);
            for (Resolver resolver : mResolvers) {
                if (resolver instanceof ScriptResolver) {
                    ScriptResolver scriptResolver = (ScriptResolver) resolver;
                    if (scriptResolver.hasUrlLookup()) {
                        scriptResolver.lookupUrl(url);
                    }
                }
            }
        }
    }

    public void reportUrlResult(String url, Resolver resolver, ScriptResolverUrlResult result) {
        Log.d(TAG, "reportUrlResult - url: " + url);
        mUrlLookupMap.put(url, result);
        Intent reportIntent = new Intent(PIPELINE_URLLOOKUPFINISHED);
        reportIntent.putExtra(PIPELINE_URLLOOKUPFINISHED_URL, url);
        reportIntent.putExtra(PIPELINE_URLLOOKUPFINISHED_RESOLVERID, resolver.getId());
        TomahawkApp.getContext().sendBroadcast(reportIntent);
    }

    public ScriptResolverUrlResult getUrlResult(String url) {
        return mUrlLookupMap.get(url);
    }

    /**
     * @return true if one or more {@link ScriptResolver}s are currently resolving. False otherwise
     */
    public boolean isResolving() {
        for (Resolver resolver : mResolvers) {
            if (resolver.isResolving() || !resolver.isReady()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return whether or not every Resolver in this PipeLine is ready to resolve queries
     */
    public boolean isEveryResolverReady() {
        if (!mAllResolversAdded) {
            return false;
        }
        for (Resolver r : mResolvers) {
            if (!r.isReady()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Callback method, which is being called by Resolvers as soon as they are ready
     */
    @Override
    public void onResolverReady(Resolver resolver) {
        if (isEveryResolverReady()) {
            for (Query query : mWaitingQueries.values()) {
                mWaitingQueries.remove(query.getCacheKey());
                resolve(query);
            }
            for (String url : mWaitingUrlLookups) {
                mWaitingUrlLookups.remove(url);
                lookupUrl(url);
            }
        }
    }

    public void setAllResolversAdded(boolean allResolversAdded) {
        mAllResolversAdded = allResolversAdded;
    }

    public void addCustomUrlHandler(String protocol, Resolver resolver, String callbackFuncName) {
        mUrlHandlerMap.put(protocol, new ResolverUrlHandler(resolver, callbackFuncName));
    }

    /**
     * Get the corresponding url handler for the given result with which a url can be translated to
     * an actual streaming url.
     *
     * @return the corresponding ResolverUrlHandler, if available. Otherwise null.
     */
    public ResolverUrlHandler getCustomUrlHandler(Result result) {
        if (result != null) {
            String path = result.getPath();
            String[] pathParts = path.split(":");
            String protocol = pathParts[0];
            return getCustomUrlHandler(protocol);
        }
        return null;
    }

    public ResolverUrlHandler getCustomUrlHandler(String protocol) {
        return mUrlHandlerMap.get(protocol);
    }
}