Java tutorial
/** * Copyright (c) 2010 Enric Ruiz, Ignasi Barrera * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.rhymestore.store; import static com.rhymestore.config.Configuration.REDIS_PASSWORD; import java.io.IOException; import java.net.URLDecoder; import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.text.Normalizer; import java.text.Normalizer.Form; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; import com.rhymestore.lang.StressType; import com.rhymestore.lang.WordParser; import com.rhymestore.lang.WordUtils; /** * Manages the Redis database to store and search rhymes. * * @author Enric Ruiz * @see Keymaker * @see Jedis * @see WordParser */ @Singleton public class RedisStore { /** The logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(RedisStore.class); /** The key used to store the next id value. */ private static final String NEXT_ID_KEY = "next.id"; /** The character encoding to use. */ private final Charset encoding; /** Redis namespace for sentences. */ private final Keymaker sentencens; /** Redis namespace for index. */ private final Keymaker indexns; /** Parses the words to get the part used to rhyme. */ private final WordParser wordParser; /** The Redis database API. */ protected final Jedis redis; /** The password used to access Redis. */ private final Optional<String> redisPassword; @Inject public RedisStore(@Named("sentence") final Keymaker sentencens, @Named("index") final Keymaker indexns, final WordParser wordParser, final Jedis redis, final @Named(REDIS_PASSWORD) Optional<String> redisPassword, final Charset encoding) { super(); this.sentencens = sentencens; this.indexns = indexns; this.wordParser = wordParser; this.redis = redis; this.redisPassword = redisPassword; this.encoding = encoding; } /** * Adds the given rhyme to the Redis database. * * @param sentence The rhyme to add. * @throws IOException If an error occurs while adding the rhyme. */ public void add(final String sentence) throws IOException { String word = WordUtils.getLastWord(sentence); if (word.isEmpty()) { return; } // Get the rhyme and type (and check that the word is valid before // adding) String rhyme = normalizeString(wordParser.phoneticRhymePart(word)); StressType type = wordParser.stressType(word); connect(); try { String sentenceKey = getUniqueId(sentencens, normalizeString(sentence)); sentenceKey = sentencens.build(sentenceKey).toString(); if (redis.exists(sentenceKey) == 1) { return; } // Insert sentence redis.set(sentenceKey, URLEncoder.encode(sentence, encoding.displayName())); // Index sentence String indexKey = getUniqueId(indexns, buildUniqueToken(rhyme, type)); indexKey = indexns.build(indexKey).toString(); redis.sadd(indexKey, sentenceKey); LOGGER.info("Added rhyme: {}", sentence); } finally { disconnect(); } } /** * Deletes the given rhyme from the Redis database. * * @param sentence The rhyme to delete. * @throws IOException If an error occurs while deleting the rhyme. */ public void delete(final String sentence) throws IOException { String word = WordUtils.getLastWord(sentence); if (word.isEmpty()) { return; } String rhyme = normalizeString(wordParser.phoneticRhymePart(word)); StressType type = wordParser.stressType(word); connect(); try { String sentenceKey = getUniqueIdKey(sentencens, normalizeString(sentence)); if (redis.exists(sentenceKey) == 0) { throw new IOException("The element to remove does not exist."); } String indexKey = getUniqueIdKey(indexns, buildUniqueToken(rhyme, type)); String sentenceId = redis.get(sentenceKey); sentenceId = sentencens.build(sentenceId).toString(); // Remove the index if (redis.exists(indexKey) == 1) { String indexId = redis.get(indexKey); indexId = indexns.build(indexId).toString(); // Remove the sentence from the index if (redis.exists(indexId) == 1) { redis.srem(indexId, sentenceId); } // Remove the index if empty if (redis.smembers(indexId).isEmpty()) { redis.del(indexId, indexKey); } } // Remove the key redis.del(sentenceId, sentenceKey); LOGGER.info("Deleted rhyme: {}", sentence); } finally { disconnect(); } } /** * Gets all the stored rhymes. * * @return A <code>Set</code> with all the stored rhymes. * @throws IOException If the rhymes cannot be obtained. */ public Set<String> findAll() throws IOException { Set<String> rhymes = new HashSet<String>(); connect(); try { String lastId = getLastId(sentencens); if (lastId != null) { Integer n = Integer.parseInt(getLastId(sentencens)); for (int i = 1; i <= n; i++) { String id = sentencens.build(String.valueOf(i)).toString(); if (redis.exists(id) == 1) { rhymes.add(URLDecoder.decode(redis.get(id), encoding.displayName())); } } } } finally { disconnect(); } return rhymes; } /** * Gets a rhyme for the given sentence. * * @param sentence The sentence to rhyme. * @return The rhyme. */ public String getRhyme(final String sentence) throws IOException { String lastWord = WordUtils.getLastWord(sentence); String rhymepart = wordParser.phoneticRhymePart(lastWord); StressType type = wordParser.stressType(lastWord); LOGGER.debug("Finding rhymes for {}", sentence); Set<String> rhymes = Sets.newHashSet(); connect(); try { rhymes.addAll(search(rhymepart, type)); } finally { disconnect(); } if (rhymes.isEmpty()) { // If no rhyme is found, return null return null; } else { // Otherwise, return a random rhyme List<String> rhymeList = new ArrayList<String>(rhymes); Random random = new Random(System.currentTimeMillis()); int index = random.nextInt(rhymeList.size()); return rhymeList.get(index); } } /** * Connects to the Redis database. * * @throws UnknownHostException If the target host does not respond. * @throws IOException If an error occurs while connecting. */ protected void connect() throws UnknownHostException, IOException { if (!redis.isConnected()) { redis.connect(); if (redisPassword.isPresent()) { redis.auth(redisPassword.get()); } } } /** * Disconnects from the Redis database. * * @throws IOException If an error occurs while disconnecting. */ protected void disconnect() throws IOException { if (redis.isConnected()) { redis.disconnect(); } } /** * Search for rhymes for the given sentence. * * @param rhyme The rhyme to search. * @param type The <code>StressType</code> of the rhyme to search. * @return A <code>Set</code> of rhymes for the given sentence. * @throws IOException If an error occurs while searching for the rhymes. */ private Set<String> search(final String rhyme, final StressType type) throws IOException { Set<String> rhymes = new HashSet<String>(); String norm = normalizeString(rhyme); String indexKey = getUniqueIdKey(indexns, buildUniqueToken(norm, type)); if (redis.exists(indexKey) == 1) { String indexId = redis.get(indexKey); indexId = indexns.build(indexId).toString(); if (redis.exists(indexId) == 1) { for (String sentenceKey : redis.smembers(indexId)) { if (redis.exists(sentenceKey) == 1) { rhymes.add(URLDecoder.decode(redis.get(sentenceKey), encoding.displayName())); } } } } return rhymes; } /** * Build a unique token for the given rhyme to be used to index it. * * @param rhyme The rhyme part of the sentence. * @param type The stress type of the rhyme. * @return The unique token for the rhyme. */ private String buildUniqueToken(final String rhyme, final StressType type) { return sum(type.name().concat(rhyme)); } /** * Get the key of the id for the given token. * * @param ns The namespace of the key. * @param token The token which key is requested. * @return The key for the given token. */ private String getUniqueIdKey(final Keymaker ns, final String token) { String md = sum(token); return ns.build(md, "id").toString(); } /** * Get a unique id id for the given token. * * @param ns The namespace of the id. * @param token The token which id is requested. * @return The id for the given token. */ private String getUniqueId(final Keymaker ns, final String token) { String key = getUniqueIdKey(ns, token); String id = redis.get(key); if (id != null) { return id; } Integer next = redis.incr(ns.build(NEXT_ID_KEY).toString()); id = next.toString(); if (redis.setnx(key, id) == 0) { id = redis.get(key); } return id; } /** * Get the last used id in the given namespace. * * @param ns The namespace. * @return The last used id in the given namespace. */ private String getLastId(final Keymaker ns) { return redis.get(ns.build(NEXT_ID_KEY).toString()); } /** * Makes a md5 sum of the given text. * * @param value The text to sum. * @return The md5 sum of the given text. */ @VisibleForTesting String sum(final String value) { return Hashing.md5().hashString(value, encoding).toString(); } /** * Normalizes the given string. * * @param value The string to be normalized. * @return The normalized string. */ private String normalizeString(final String value) { // To lower case String token = value.toLowerCase(); // Remove diacritics token = Normalizer.normalize(token, Form.NFD); token = token.replaceAll("[^\\p{ASCII}]", ""); // Remove non alphanumeric characters token = token.replaceAll("[^a-zA-Z0-9]", ""); return token; } }