/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat Jomon, Copyright (c) 2012-2013 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jomon.runtime.util;

import javax.annotation.Nonnull;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import static java.util.regex.Pattern.*;
import static org.echocat.jomon.runtime.StringUtils.*;
import static org.echocat.jomon.runtime.util.GlobPattern.*;

/**
 * A simple (but fast ;) pattern-matcher. Matches strings against a pattern
 * that may contain the wildcards '*' and '?'.
 *
 * Occurences of the wildcards can be escaped using '\'. The '\' has to be
 * escaped by '\\'.
 * Matching of '*' is non-greedy. '*' matches 0 or more, '?' matches exact one.
 */
public class Glob {

    private String _patternSource;
    /**
     * A List of GlobPattern objects.
     */
    private GlobPattern[] _compiledPattern;
    /**
     * If true String matching will be done case insensitivly.
     */
    private final boolean _caseInSensitive;

    /**
     * @return true if the given pattern contains globbing AND is valid.
     */
    public static boolean containsGlob(String pattern) {
        boolean result = false;
        try {
            final Glob glob = new Glob(pattern);
            result = glob.containsGlob();
        } catch (final ParseException e) {
            // Ignored
        }
        return result;
    }

    public Glob(String pattern) throws ParseException {
        this(pattern, false);
    }

    public Glob(String pattern, boolean caseInSensitive) throws ParseException {
        _patternSource = pattern;
        _caseInSensitive = caseInSensitive;
        compilePattern();
    }

    /**
     * Match the given string.
     */
    public boolean matches(String string) {
        String src = string;
        final boolean result;
        if (_compiledPattern.length == 1) {

            // Shortcut for pattern '*', '?' and patterns not containing any wildcard
            final GlobPattern pattern = _compiledPattern[0];
            if (pattern.getType() == GLOB_MULTIPLE) {
                result = true;
            } else if (pattern.getType() == GLOB_SINGLE) {
                result = src.length() == 1;
            } else {
                if (_caseInSensitive) {
                    result = src.equalsIgnoreCase(pattern.getText());
                } else {
                    result = src.equals(pattern.getText());
                }
            }

        } else if (_compiledPattern.length == 2) {

            // Shortcuts for common patterns '*something' and 'something*'
            final GlobPattern pattern1 = _compiledPattern[0];
            final GlobPattern pattern2 = _compiledPattern[1];
            if (_caseInSensitive) {
                src = src.toLowerCase();
            }
            if (pattern1.getType() == TEXT) {
                 result = src.startsWith(pattern1.getText()) && (pattern2.getType() == GLOB_MULTIPLE || src.length() == pattern1.getTextLength()+1);
            } else {
                result = src.endsWith(pattern2.getText()) && (pattern1.getType() == GLOB_MULTIPLE || src.length() == pattern2.getTextLength()+1);
            }

        } else {
            result = matches(src.toCharArray());
        }
        return result;
    }

    /**
     * Match the given char-Array.
     */
    public boolean matches(char[] src) {

        if (_caseInSensitive) {
            toLowerCase(src);
        }

        return matchIntern(src, 0, 0);
    }

    @SuppressWarnings({"MethodWithMultipleReturnPoints", "ValueOfIncrementOrDecrementUsed"})
    private boolean matchIntern(char[] csrc, int pos, int currentSpos) {
        GlobPattern pattern;
        int spos = currentSpos;
        for (int p = pos; p < _compiledPattern.length; p++) {
            pattern = _compiledPattern[p];
            if (pattern.getType() == TEXT) {

                // Match the text
                if (!startsWith(csrc, pattern.getChars(), spos)) {
                    return false;
                }

                // Text match ok.
                spos += pattern.getTextLength();
            } else if (pattern.getType() == GLOB_SINGLE) {

                // Single globbing
                if (spos++ == csrc.length) {
                    return false;
                }
            } else {

                // Shortcut: No more text elements -> end it.
                if (pattern.getNextTextElement() == null) {
                    return true;
                }

                // Another shortcut: Is the next textnode
                final char[] chars = pattern.getNextTextElement().getChars();
                int substrPos = indexOf(csrc, chars, spos);

                // Go through all possible ways
                while (substrPos != -1) {

                    // Optimization: Is the next element after the textelement
                    // a multiglob? Or is it a singleglob and the length matches?
                    // Then we are through
                    if (p == _compiledPattern.length - 3) {
                        final GlobPattern p2 = _compiledPattern[_compiledPattern.length - 1];
                        if (p2.getType() == GLOB_MULTIPLE) {
                            return true;
                        }
                        if (p2.getType() == GLOB_SINGLE && substrPos + chars.length == csrc.length - 1) {
                            return true;
                        }
                    }

                    // Optimization: Since we know that the next part is a textelement
                    // and we know that it fits -> we can skip that element
                    if (p < _compiledPattern.length - 2) {
                        substrPos += chars.length;
                    } else {

                        // Now we know that we are though! The next one is a textelement
                        // and it matches
                        return true;
                    }
                    if (matchIntern(csrc, p + 2, substrPos)) {
                        return true;
                    }
                    substrPos = indexOf(csrc, chars, substrPos + 1);
                }

                // Oh - it didn't work out.
                return false;
            }
        }
        return csrc.length == spos;
    }

    public boolean containsGlob() {
        return ! (getCompiledPattern().length == 1 && getCompiledPattern()[0].getType() == TEXT);
    }

    public boolean isCaseInSensitive() {
        return _caseInSensitive;
    }

    /**
     * Compile the pattern into the strings[] and wildcard[] arrays.
     */
    private void compilePattern() throws ParseException {
        StringBuilder sb = new StringBuilder();
        boolean escaped = false;
        final List<GlobPattern> patterns = new ArrayList<>();
        for (int a = 0; a < _patternSource.length(); a++) {
            final char c = _patternSource.charAt(a);

            // Test escape-char
            if (c == '\\') {
                if (escaped) {
                    sb.append('\\');
                    escaped = false;
                } else {
                    escaped = true;
                }
            } else {
                if (escaped) {
                    if (c != GLOB_SINGLE && c != GLOB_MULTIPLE) {
                        sb.append("\\");
                    } else {

                        // Globbing chars escaped
                        sb.append(c);
                        escaped = false;
                    }
                } else {

                    // No escape character, test whether it matches on of the globbing characters.
                    if (c == GLOB_MULTIPLE || c == GLOB_SINGLE) {
                        if (sb.length() > 0) {
                            patterns.add(new GlobPattern(TEXT, sb.toString()));
                            sb = new StringBuilder();
                        }
                        patterns.add(new GlobPattern(c));
                    } else {
                        sb.append(c);
                    }
                }
            }
        }
        if (sb.length() > 0) {
            patterns.add(new GlobPattern(TEXT, sb.toString()));
        }

        // Add meta information and correct the elements
        addMetaInformation(patterns);

        _compiledPattern = new GlobPattern[patterns.size()];
        patterns.toArray(_compiledPattern);
    }

    @SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "ContinueStatement", "AssignmentToForLoopParameter", "BreakStatement"})
    private void addMetaInformation(List<GlobPattern> patterns) {
        for (int a = 0; a < patterns.size(); a++) {
            final GlobPattern p = patterns.get(a);
            if (_caseInSensitive && p.getType() == TEXT) {
                p.setText(p.getText().toLowerCase());
            }
            for (int b = a + 1; b < patterns.size(); b++) {
                final GlobPattern p2 = patterns.get(b);

                // Remove things like '**', '?*' or '*?'
                if (p.getType() == GLOB_MULTIPLE) {
                    if (p2.getType() == GLOB_MULTIPLE || p2.getType() == GLOB_SINGLE) {
                        patterns.remove(b--);
                        continue;
                    }
                }
                if (p.getType() == GLOB_SINGLE) {
                    if (p2.getType() == GLOB_MULTIPLE) {
                        patterns.remove(b--);
                        continue;
                    }
                }

                if (p2.getType() == TEXT) {
                    p.setNextTextElement(p2);
                    break;
                }
            }
        }
    }

    public GlobPattern[] getCompiledPattern() {
        return _compiledPattern;
    }

    @Nonnull
    public Pattern toPattern() {
        final StringBuilder sb = new StringBuilder();
        sb.append("^");
        for (final GlobPattern pattern : _compiledPattern) {
            final char type = pattern.getType();
            if (type == GLOB_MULTIPLE) {
                sb.append(".*");
            } else if (type == GLOB_SINGLE) {
                sb.append(".");
            } else if (type == TEXT) {
                sb.append(quote(pattern.getText()));
            } else {
                throw new IllegalStateException("Don't know how to handle type " + type + ".");
            }
        }
        sb.append("$");
        return compile(sb.toString(), _caseInSensitive ? CASE_INSENSITIVE : 0);
    }

    public String getPatternSource() {
        return _patternSource;
    }

    public void setPatternSource(String patternSource) throws ParseException {
        _patternSource = patternSource;
        compilePattern();
    }

    @Override
    public String toString() {
        return _patternSource;
    }
}


/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat Jomon, Copyright (c) 2012-2013 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jomon.cache;

import javax.annotation.Nonnull;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import javax.annotation.concurrent.ThreadSafe;

/**
 *
A Cache based on the Least Recently Used algorithm. If the cache is full then the element that hasn't been access for the biggest period of time will
 * be removed.

 */
@ThreadSafe
public class LruCache<K, V> extends InMemoryBasedCacheSupport<K, V> {

    public LruCache(@Nonnull Class<? extends K> keyType, @Nonnull Class<? extends V> valueType) {
        super(keyType, valueType);
    }

    /**
     * Add the new CacheEntry before the rest.
     */
    @Override
    @OverridingMethodsMustInvokeSuper
    protected void updateListAfterPut(CacheEntry<K, V> newEntry) {
        if (_last == null) {
            _last = newEntry;
            _first = newEntry;
        } else {
            _first.setPrevious(newEntry);
            newEntry.setNext(_first);
            setFirst(newEntry);
        }
    }

    /**
     * Move the entry to the beginning of the LinkedList.
     */
    @Override
    protected void updateListAfterHit(CacheEntry<K, V> entry) {
        if (entry != null && !entry.equals(_first)) {
            if (entry.equals(_last)) {
                setLast(entry.getPrevious());
            } else {
                final CacheEntry<K, V> previous = entry.getPrevious();
                final CacheEntry<K, V> next = entry.getNext();

                previous.setNext(next);
                next.setPrevious(previous);
            }
            _first.setPrevious(entry);
            entry.setNext(_first);
            setFirst(entry);
        }
    }
}


/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat Jomon, Copyright (c) 2012-2014 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jomon.cache;

import org.echocat.jomon.runtime.iterators.CloseableIterator;
import org.echocat.jomon.runtime.util.*;
import org.echocat.jomon.runtime.util.Value.Fixed;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import java.util.*;
import java.util.Map.Entry;

import static java.lang.System.currentTimeMillis;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.echocat.jomon.runtime.CollectionUtils.asCloseableIterator;

/**
 *
Synopsis

 *
Base implementation using a map and CacheEntry instances. Note that due to the ongoing reordering of elements the implementation uses a lot of
 * synchronization. The order of the elements is maintained by a linked list ({@link CacheEntry})

 *
 *
The first and the last element is stored internally in the members {@link #_first} and {@link #_last} (surprisingly naming! ;).

 */
@SuppressWarnings("ProtectedField")
@ThreadSafe
public abstract class InMemoryBasedCacheSupport<K, V> extends CacheSupport<K, V> implements StatisticsEnabledCache<K, V>, LimitedCache<K, V>, ClearableCache<K, V>, ListenerEnabledCache<K, V>, IdentifiedCache<K, V>, ProducingTypeEnabledCache<K, V>, KeysEnabledCache<K, V>, AutoCloseable {

    protected final Object _lock = new Object();
    protected final CacheListenerInvoker _listenerInvoker = new CacheListenerInvoker();
    protected final long _createdTimestamp;

    protected String _id;
    protected Map<K, CacheEntry<K, V>> _entries;
    protected Integer _capacity;
    protected Duration _defaultExpireAfter;
    private ProducingType _producingType = ProducingType.DEFAULT;

    protected long _numberOfRequests;
    protected long _numberOfHits;
    protected long _numberOfDrops;
    protected CacheEntry<K, V> _first;
    protected CacheEntry<K, V> _last;
    protected long _nearstExpiringTime;

    protected InMemoryBasedCacheSupport(@Nonnull Class<? extends K> keyType, @Nonnull Class<? extends V> valueType) {
        super(keyType, valueType);
        _createdTimestamp = currentTimeMillis();
        _entries = new HashMap<>();
        _first = null;
        _last = null;
        _numberOfHits = 0;
        _numberOfRequests = 0;
        _nearstExpiringTime = currentTimeMillis();
    }

    @Override
    public String getId() {
        return _id;
    }

    @Override
    public void setId(String id) {
        _id = id;
    }

    @Override
    public Duration getMaximumLifetime() {
        return _defaultExpireAfter;
    }

    @Override
    public Long getCapacity() {
        return _capacity != null ? _capacity.longValue() : null;
    }

    @Override
    public void setListeners(@Nullable Collection<CacheListener> listeners) {
        _listenerInvoker.setListeners(listeners);
    }

    @Override
    public Collection<CacheListener> getListeners() {
        return _listenerInvoker.getListeners();
    }

    @Override
    @Nonnull
    public ProducingType getProducingType() {
        return _producingType;
    }

    @Override
    public void setProducingType(@Nonnull ProducingType producingType) {
        if (producingType == null) {
            throw new NullPointerException();
        }
        _producingType = producingType;
    }

    /**
     * Remove the last element from the cache.
     */
    protected void removeLast() {
        final CacheEntry<K, V> entry;
        synchronized (_lock) {
            if (_last != null) {
                entry = _entries.remove(_last.getKey());
                setLast(_last.getPrevious());
            } else {
                entry = null;
            }
            if (size() == 0) {
                _first = null;
                _last = null;
            }
        }
        if (entry != null) {
            handleRemove(entry);
        }
    }

    /**
     * Remove entries that are out of their maxLifetime.
     */
    @SuppressWarnings("UnnecessaryUnboxing")
    public void cleanUpLifetimeExpired() {
        final long currentTime = currentTimeMillis();
        // Test whether there could be an element that has to be removed
        if (_nearstExpiringTime > 0 && _nearstExpiringTime <= currentTime) {
            // Go through all elements and remove elements that are outdated
            final List<K> keysToRemove = new ArrayList<>();
            synchronized (_lock) {
                _nearstExpiringTime = 0;
                for (final CacheEntry<K, V> cacheEntry : _entries.values()) {
                    final Long expire = cacheEntry.getExpire();
                    if (expire != null) {
                        final long unboxedExpire = expire.longValue();
                        if (unboxedExpire <= currentTime) {
                            keysToRemove.add(cacheEntry.getKey());
                        } else if (_nearstExpiringTime == 0 || unboxedExpire < _nearstExpiringTime) {
                            _nearstExpiringTime = unboxedExpire;
                        }
                    }
                }
                for (final K key : keysToRemove) {
                    removeInternal(key);
                }
            }
        }
    }

    @Override
    public void clear() {
        if (_listenerInvoker.beforeClear(this)) {
            final Map<K, CacheEntry<K, V>> oldEntries;
            synchronized (_lock) {
                oldEntries = _entries;
                _entries = new HashMap<>();
                _first = null;
                _last = null;
                resetStatistics();
            }
            for (final CacheEntry<K, V> entry : oldEntries.values()) {
                try {
                    handleRemove(entry);
                } catch (final ValueProducingFailedException ignored) {}
            }
            _listenerInvoker.afterClear(this);
        }
    }

    /**
     * Is called by the {@link #get} method, must be implemented according to caching strategy.
     */
    protected abstract void updateListAfterHit(CacheEntry<K, V> entry);

    @Override
    public V get(K key) {
        checkKey(key);
        final V value;
        if (_listenerInvoker.beforeGet(this, key)) {
            CacheEntry<K, V> cacheEntry;
            CacheEntry<K, V> outdatedCacheEntry = null;
            synchronized (_lock) {
                _numberOfRequests++;
                cacheEntry = _entries.get(key);
                if (cacheEntry != null) {
                    if (isOutDated(cacheEntry)) {
                        // An outdated entry, remove it ...
                        outdatedCacheEntry = internalRemove(key);
                        cacheEntry = null;
                    } else {
                        _numberOfHits++;
                        cacheEntry.hit();
                        updateListAfterHit(cacheEntry);
                    }
                }
            }
            if (outdatedCacheEntry != null) {
                handleRemove(outdatedCacheEntry);
            }
            final Value<V> valueHolder = cacheEntry == null ? null : cacheEntry.getValue();
            value = valueHolder != null ? valueHolder.getValue() : null;
            _listenerInvoker.afterGet(this, key, valueHolder);
        } else {
            value = null;
        }
        return value;
    }

    @Override
    public V get(@Nullable K key, @Nullable ValueProducer<K, V> cacheValueProducer) {
        return get(key, cacheValueProducer, null);
    }

    @Override
    public V get(@Nullable K key, @Nullable ValueProducer<K, V> cacheValueProducer, @Nullable Duration expireAfter) {
        checkKey(key);
        final V value;
        if (_listenerInvoker.beforeGet(this, key)) {
            CacheEntry<K, V> cacheEntry;
            CacheEntry<K, V> outdatedCacheEntry = null;
            synchronized (_lock) {
                _numberOfRequests++;
                cacheEntry = _entries.get(key);
                if (cacheEntry != null) {
                    if (isOutDated(cacheEntry)) {
                        // An outdated entry, remove it ...
                        outdatedCacheEntry = internalRemove(key);
                        cacheEntry = null;
                    } else {
                        _numberOfHits++;
                        cacheEntry.hit();
                        updateListAfterHit(cacheEntry);
                    }
                }
                if (cacheEntry == null && cacheValueProducer != null) {
                    cacheEntry = new CacheEntry<>(key, getTargetExpireAfterBasedOn(expireAfter), cacheValueProducer, _producingType);
                    internalPut(cacheEntry);
                }
            }
            if (outdatedCacheEntry != null) {
                handleRemove(outdatedCacheEntry);
            }
            final Value<V> valueHolder = cacheEntry != null ? cacheEntry.getValue() : null;
            value = valueHolder != null ? valueHolder.getValue() : null;
            checkValueAfterProducing(value);
            _listenerInvoker.afterGet(this, key, valueHolder);
        } else {
            value = null;
        }
        return value;
    }

    @Override
    public boolean contains(K key) {
        checkKey(key);
        synchronized (_lock) {
            get(key);
            return _entries.containsKey(key);
        }
    }

    /**
     * This is called by the {@link #put} method, must be implemented according to caching strategy.
     */
    protected abstract void updateListAfterPut(CacheEntry<K, V> newEntry);

    @Override
    public void put(@Nullable K key, @Nullable V value) {
        put(key, value, null);
    }

    @Override
    public void put(@Nullable K key, @Nullable V value, @Nullable Duration expireAfter) {
        checkKey(key);
        checkValue(value);
        final Fixed<V> fixed = new Fixed<>(value);
        if (_listenerInvoker.beforePut(this, key, fixed, expireAfter)) {
            final Long targetExpireAfter = getTargetExpireAfterBasedOn(expireAfter);
            final CacheEntry<K, V> newEntry = new CacheEntry<>(key, targetExpireAfter, value);
            internalPut(newEntry);
            _listenerInvoker.afterPut(this, key, fixed, expireAfter);
        }
    }

    @Nullable
    protected Long getTargetExpireAfterBasedOn(@Nullable Duration expireAfter) {
        final Long targetExpireAfter;
        if (expireAfter != null) {
            targetExpireAfter = expireAfter.in(MILLISECONDS);
        } else {
            final Duration defaultExpireAfter = _defaultExpireAfter;
            targetExpireAfter = defaultExpireAfter != null ? defaultExpireAfter.in(MILLISECONDS) : null;
        }
        return targetExpireAfter;
    }

    @Override
    @Nullable
    public Value<V> remove(@Nullable K key) {
        checkKey(key);
        final Value<V> result;
        if (_listenerInvoker.beforeRemove(this, key)) {
            result = removeInternal(key);
            _listenerInvoker.afterRemove(this, key, result);
        } else {
            result = null;
        }
        return result;
    }

    @Nullable
    protected Value<V> removeInternal(@Nullable K key) {
        final Value<V> result;
        final CacheEntry<K, V> removedCacheEntry = internalRemove(key);
        if (removedCacheEntry == null) {
            result = null;
        } else {
            result = removedCacheEntry.getValue();
            handleRemove(removedCacheEntry);
        }
        return result;
    }

    protected void handleRemove(@Nullable CacheEntry<K, V> cacheEntry) {
        _numberOfDrops++;
    }

    protected void setFirst(@Nullable CacheEntry<K, V> cacheEntry) {
        synchronized (_lock) {
            if (cacheEntry == null) {
                _first = null;
                _last = null;
            } else {
                if (_first == null) {
                    _last = cacheEntry;
                    _last.setNext(null);
                }
                _first = cacheEntry;
                _first.setPrevious(null);
            }
        }
    }

    protected void setLast(@Nullable CacheEntry<K, V> cacheEntry) {
        synchronized (_lock) {
            if (_last == null) {
                _first = cacheEntry;
                if (_first != null) {
                    _first.setPrevious(null);
                }
            }
            _last = cacheEntry;
            if (_last != null) {
                _last.setNext(null);
            }
        }
    }

    @Override
    public void setMaximumLifetime(@Nullable Duration maxLifetime) {
        if (_listenerInvoker.beforeSetMaximumLifetime(this, maxLifetime)) {
            _defaultExpireAfter = maxLifetime;
            _listenerInvoker.afterSetMaximumLifetime(this, maxLifetime);
        }
    }

    @Override
    public void setCapacity(@Nullable Long capacity) {
        if (capacity != null && capacity > Integer.valueOf(Integer.MAX_VALUE).longValue()) {
            throw new IllegalArgumentException("The capacity does not reach " + Integer.MAX_VALUE + ".");
        }
        final List<CacheEntry<K, V>> toCleanUp;
        synchronized (_lock) {
            _capacity = capacity != null ? capacity.intValue() : null;
            final Map<K, CacheEntry<K, V>> old = _entries;
            toCleanUp = new ArrayList<>();
            _entries = _capacity != null ? new HashMap<K, CacheEntry<K, V>>(_capacity, 1f) : new HashMap<K, CacheEntry<K, V>>();
            int i = 0;
            for (final Entry<K, CacheEntry<K, V>> entry : old.entrySet()) {
                if (i < capacity) {
                    _entries.put(entry.getKey(), entry.getValue());
                } else {
                    toCleanUp.add(entry.getValue());
                }
                i++;
            }
        }
        for (final CacheEntry<K, V> entry : toCleanUp) {
            try {
                handleRemove(entry);
            } catch (final ValueProducingFailedException ignored) {
            }
        }
    }

    @Override
    public Long size() {
        synchronized (_lock) {
            return (long) _entries.size();
        }
    }

    @Override
    public Long getNumberOfHits() {
        return _numberOfHits;
    }

    @Override
    public Long getNumberOfRequests() {
        return _numberOfRequests;
    }

    @Override
    public Long getNumberOfDrops() {
        return _numberOfDrops;
    }

    @Override
    public Date getCreated() {
        return new Date(_createdTimestamp);
    }

    protected CacheEntry<K, V> getFirst() {
        synchronized (_lock) {
            return _first;
        }
    }

    protected CacheEntry<K, V> getLast() {
        synchronized (_lock) {
            return _last;
        }
    }

    protected Object getLock() {
        return _lock;
    }

    /**
     * You must synchronize on the {@link #getLock() semaphore}
     * while operating on the returned entries.
     */
    protected Map<K, CacheEntry<K, V>> getEntries() {
        return _entries;
    }

    private boolean isOutDated(CacheEntry<K, V> cacheEntry) {
        final Long expire = cacheEntry.getExpire();
        return expire != null && expire <= currentTimeMillis();
    }

    /**
     * @return the removed {@link CacheEntry} or null if no cache entry was removed
     */
    private CacheEntry<K, V> internalRemove(K key) {
        final CacheEntry<K, V> entry;
        synchronized (_lock) {
            if (_entries.isEmpty()) {
                _first = null;
                _last = null;
                entry = null;
            } else {
                entry = _entries.remove(key);
                if (entry != null) {
                    // Remove the entry from the LinkedList
                    // noinspection ObjectEquality
                    if (entry == _first) {
                        setFirst(_first.getNext());
                    } else {
                        //noinspection ObjectEquality
                        if (entry == _last) {
                            setLast(_last.getPrevious());
                        } else {
                            final CacheEntry<K, V> previous = entry.getPrevious();
                            final CacheEntry<K, V> next = entry.getNext();
                            previous.setNext(next);
                            next.setPrevious(previous);
                        }
                    }
                }
            }
        }
        return entry;
    }

    private void internalPut(CacheEntry<K, V> newEntry) {
        synchronized (_lock) {
            final Integer maxSize = _capacity;
            if (maxSize != null && size() >= maxSize) {
                // max size reached, remove outdated cache entries ...
                cleanUpLifetimeExpired();
                if (size() >= maxSize) {
                    // still to many cache entries, remove last entry (depends on the implementation) ...
                    removeLast();
                }
            }
            final K key = newEntry.getKey();
            final Long expire = newEntry.getExpire();
            if (_nearstExpiringTime > 0 && expire != null && expire <= _nearstExpiringTime) {
                _nearstExpiringTime = expire;
            }
            final CacheEntry<K, V> oldEntry = _entries.put(key, newEntry);
            if (oldEntry != null) {
                // This should not happen very often, but we need to ensure,
                // that there aren't two entries with the same key in the list,
                // so we put oldEntry back into the map ...
                _entries.put(key, oldEntry);
                // ... replace its value ...
                handleRemove(oldEntry);
                oldEntry.setValue(newEntry.getExpire(), newEntry.getValue());
                // ... and simulate a hit ...
                _numberOfHits++;
                oldEntry.hit();
                updateListAfterHit(oldEntry);
            } else {
                updateListAfterPut(newEntry);
            }
        }
    }

    @Override
    public void resetStatistics() {
        if (_listenerInvoker.beforeResetStatistics(this)) {
            _numberOfDrops = 0;
            _numberOfHits = 0;
            _numberOfRequests = 0;
            _listenerInvoker.afterResetStatistics(this);
        }
    }

    @Override
    public CloseableIterator<K> iterator() {
        final Set<K> keys;
        synchronized (_lock) {
            keys = new HashSet<>(_entries.keySet());
        }
        return asCloseableIterator(keys.iterator());
    }

    @Override
    public void close() throws Exception {
        clear();
    }
}


/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat Jomon, Copyright (c) 2012-2013 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jomon.net.dns;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.*;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.*;
import java.net.*;
import java.util.*;

import static java.lang.Thread.currentThread;
import static org.apache.commons.io.IOUtils.closeQuietly;

/**
 * @author Brian Wellington <bwelling@xbill.org>
 */

@SuppressWarnings({"ContinueStatement", "OverlyLongMethod", "MethodWithMultipleReturnPoints", "AssignmentToMethodParameter", "InfiniteLoopStatement", "StatementWithEmptyBody"})
public class DnsServer implements AutoCloseable {

    private static final Logger LOG = LoggerFactory.getLogger(DnsServer.class);

    private volatile boolean _closed;
    private final Set<Thread> _threads = new HashSet<>();
    private final Set<Closeable> _closeables = new HashSet<>();

    static final int FLAG_DNSSECOK = 1;
    static final int FLAG_SIGONLY = 2;

    private final Map<Integer, Cache> _caches;
    private final Map<Name, Zone> _znames;
    private final Map<Name, TSIG> _tsigs;

    private static String addrport(InetAddress addr, int port) {
        return addr.getHostAddress() + "#" + port;
    }

    public DnsServer() {
        this(null);
    }

    public DnsServer(@Nullable String config) {
        final BufferedReader reader = new BufferedReader(new StringReader(config));
        _caches = new HashMap<>();
        _znames = new HashMap<>();
        _tsigs = new HashMap<>();
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                final StringTokenizer st = new StringTokenizer(line);
                if (!st.hasMoreTokens()) {
                    continue;
                }
                final String keyword = st.nextToken();
                if (!st.hasMoreTokens()) {
                    throw new IllegalArgumentException("Invalid line: " + line);
                }
                if (keyword.charAt(0) == '#') {
                    continue;
                }
                if (keyword.equals("primary")) {
                    addPrimaryZone(st.nextToken(), st.nextToken());
                } else if (keyword.equals("secondary")) {
                    addSecondaryZone(st.nextToken(), st.nextToken());
                } else if (keyword.equals("cache")) {
                    final Cache cache = new Cache(st.nextToken());
                    _caches.put(DClass.IN, cache);
                } else if (keyword.equals("key")) {
                    final String s1 = st.nextToken();
                    final String s2 = st.nextToken();
                    if (st.hasMoreTokens()) {
                        addTSIG(s1, s2, st.nextToken());
                    } else {
                        addTSIG("hmac-md5", s1, s2);
                    }
                } else {
                    throw new IllegalArgumentException("unknown keyword: " + keyword);
                }
            }
        } catch (IOException | ZoneTransferException e) {
            throw new RuntimeException("Could not create server.", e);
        }
        LOG.info("jnamed: running");
    }

    public void addPrimaryZone(String zname, String zonefile) throws IOException {
        Name origin = null;
        if (zname != null) {
            origin = Name.fromString(zname, Name.root);
        }
        final Zone newzone = new Zone(origin, zonefile);
        addPrimaryZone(newzone);
    }

    public void addPrimaryZone(@Nonnull Zone zone) throws IOException {
        _znames.put(zone.getOrigin(), zone);
    }

    public void addSecondaryZone(String zone, String remote)
        throws IOException, ZoneTransferException {
        final Name zname = Name.fromString(zone, Name.root);
        final Zone newzone = new Zone(zname, DClass.IN, remote);
        _znames.put(zname, newzone);
    }

    public void addSecondaryZone(@Nonnull Zone zone) {
        _znames.put(zone.getOrigin(), zone);
    }

    public void addTSIG(String algstr, String namestr, String key) throws IOException {
        final Name name = Name.fromString(namestr, Name.root);
        _tsigs.put(name, new TSIG(algstr, namestr, key));
    }

    public Cache getCache(int dclass) {
        Cache c = _caches.get(dclass);
        if (c == null) {
            c = new Cache(dclass);
            _caches.put(dclass, c);
        }
        return c;
    }

    public Zone findBestZone(Name name) {
        Zone foundzone;
        foundzone = _znames.get(name);
        if (foundzone != null) {
            return foundzone;
        }
        final int labels = name.labels();
        for (int i = 1; i < labels; i++) {
            final Name tname = new Name(name, i);
            foundzone = _znames.get(tname);
            if (foundzone != null) {
                return foundzone;
            }
        }
        return null;
    }

    public RRset findExactMatch(Name name, int type, int dclass, boolean glue) {
        final Zone zone = findBestZone(name);
        if (zone != null) {
            return zone.findExactMatch(name, type);
        } else {
            final RRset[] rrsets;
            final Cache cache = getCache(dclass);
            if (glue) {
                rrsets = cache.findAnyRecords(name, type);
            } else {
                rrsets = cache.findRecords(name, type);
            }
            if (rrsets == null) {
                return null;
            } else {
                return rrsets[0]; /* not quite right */
            }
        }
    }

    void addRRset(Name name, Message response, RRset rrset, int section, int flags) {
        for (int s = 1; s <= section; s++) {
            if (response.findRRset(name, rrset.getType(), s)) {
                return;
            }
        }
        if ((flags & FLAG_SIGONLY) == 0) {
            final Iterator<?> it = rrset.rrs();
            while (it.hasNext()) {
                Record r = (Record) it.next();
                if (r.getName().isWild() && !name.isWild()) {
                    r = r.withName(name);
                }
                response.addRecord(r, section);
            }
        }
        if ((flags & (FLAG_SIGONLY | FLAG_DNSSECOK)) != 0) {
            final Iterator<?> it = rrset.sigs();
            while (it.hasNext()) {
                Record r = (Record) it.next();
                if (r.getName().isWild() && !name.isWild()) {
                    r = r.withName(name);
                }
                response.addRecord(r, section);
            }
        }
    }

    private void addSOA(Message response, Zone zone) {
        response.addRecord(zone.getSOA(), Section.AUTHORITY);
    }

    private void addNS(Message response, Zone zone, int flags) {
        final RRset nsRecords = zone.getNS();
        addRRset(nsRecords.getName(), response, nsRecords,
            Section.AUTHORITY, flags);
    }

    private void addCacheNS(Message response, Cache cache, Name name) {
        final SetResponse sr = cache.lookupRecords(name, Type.NS, Credibility.HINT);
        if (!sr.isDelegation()) {
            return;
        }
        final RRset nsRecords = sr.getNS();
        final Iterator<?> it = nsRecords.rrs();
        while (it.hasNext()) {
            final Record r = (Record) it.next();
            response.addRecord(r, Section.AUTHORITY);
        }
    }

    private void addGlue(Message response, Name name, int flags) {
        final RRset a = findExactMatch(name, Type.A, DClass.IN, true);
        if (a == null) {
            return;
        }
        addRRset(name, response, a, Section.ADDITIONAL, flags);
    }

    private void addAdditional2(Message response, int section, int flags) {
        final Record[] records = response.getSectionArray(section);
        for (int i = 0; i < records.length; i++) {
            final Record r = records[i];
            final Name glueName = r.getAdditionalName();
            if (glueName != null) {
                addGlue(response, glueName, flags);
            }
        }
    }

    private void addAdditional(Message response, int flags) {
        addAdditional2(response, Section.ANSWER, flags);
        addAdditional2(response, Section.AUTHORITY, flags);
    }

    byte addAnswer(Message response, Name name, int type, int dclass,
              int iterations, int flags) {
        final SetResponse sr;
        byte rcode = Rcode.NOERROR;

        if (iterations > 6) {
            return Rcode.NOERROR;
        }

        if (type == Type.SIG || type == Type.RRSIG) {
            type = Type.ANY;
            flags |= FLAG_SIGONLY;
        }

        final Zone zone = findBestZone(name);
        if (zone != null) {
            sr = zone.findRecords(name, type);
        } else {
            final Cache cache = getCache(dclass);
            sr = cache.lookupRecords(name, type, Credibility.NORMAL);
        }

        if (sr.isUnknown()) {
            addCacheNS(response, getCache(dclass), name);
        }
        if (sr.isNXDOMAIN()) {
            response.getHeader().setRcode(Rcode.NXDOMAIN);
            if (zone != null) {
                addSOA(response, zone);
                if (iterations == 0) {
                    response.getHeader().setFlag(Flags.AA);
                }
            }
            rcode = Rcode.NXDOMAIN;
        } else if (sr.isNXRRSET()) {
            if (zone != null) {
                addSOA(response, zone);
                if (iterations == 0) {
                    response.getHeader().setFlag(Flags.AA);
                }
            }
        } else if (sr.isDelegation()) {
            final RRset nsRecords = sr.getNS();
            addRRset(nsRecords.getName(), response, nsRecords,
                Section.AUTHORITY, flags);
        } else if (sr.isCNAME()) {
            final CNAMERecord cname = sr.getCNAME();
            final RRset rrset = new RRset(cname);
            addRRset(name, response, rrset, Section.ANSWER, flags);
            if (zone != null && iterations == 0) {
                response.getHeader().setFlag(Flags.AA);
            }
            rcode = addAnswer(response, cname.getTarget(),
                type, dclass, iterations + 1, flags);
        } else if (sr.isDNAME()) {
            final DNAMERecord dname = sr.getDNAME();
            RRset rrset = new RRset(dname);
            addRRset(name, response, rrset, Section.ANSWER, flags);
            final Name newname;
            try {
                newname = name.fromDNAME(dname);
            } catch (final NameTooLongException ignored) {
                return Rcode.YXDOMAIN;
            }
            rrset = new RRset(new CNAMERecord(name, dclass, 0, newname));
            addRRset(name, response, rrset, Section.ANSWER, flags);
            if (zone != null && iterations == 0) {
                response.getHeader().setFlag(Flags.AA);
            }
            rcode = addAnswer(response, newname, type, dclass,
                iterations + 1, flags);
        } else if (sr.isSuccessful()) {
            final RRset[] rrsets = sr.answers();
            for (int i = 0; i < rrsets.length; i++) {
                addRRset(name, response, rrsets[i],
                    Section.ANSWER, flags);
            }
            if (zone != null) {
                addNS(response, zone, flags);
                if (iterations == 0) {
                    response.getHeader().setFlag(Flags.AA);
                }
            } else {
                addCacheNS(response, getCache(dclass), name);
            }
        }
        return rcode;
    }

    byte[] doAXFR(Name name, Message query, TSIG tsig, TSIGRecord qtsig, Socket s) {
        final Zone zone = _znames.get(name);
        boolean first = true;
        if (zone == null) {
            return errorMessage(query, Rcode.REFUSED);
        }
        final Iterator<?> it = zone.AXFR();
        try {
            final DataOutputStream dataOut;
            dataOut = new DataOutputStream(s.getOutputStream());
            final int id = query.getHeader().getID();
            while (it.hasNext()) {
                final RRset rrset = (RRset) it.next();
                final Message response = new Message(id);
                final Header header = response.getHeader();
                header.setFlag(Flags.QR);
                header.setFlag(Flags.AA);
                addRRset(rrset.getName(), response, rrset,
                    Section.ANSWER, FLAG_DNSSECOK);
                if (tsig != null) {
                    tsig.applyStream(response, qtsig, first);
                    qtsig = response.getTSIG();
                }
                first = false;
                final byte[] out = response.toWire();
                dataOut.writeShort(out.length);
                dataOut.write(out);
            }
        } catch (final IOException ignored) {
            LOG.info("AXFR failed");
        }
        closeQuietly(s);
        return null;
    }

    /*
    * Note: a null return value means that the caller doesn't need to do
    * anything.  Currently this only happens if this is an AXFR request over
    * TCP.
    */
    byte[] generateReply(Message query, byte[] in, int length, Socket s)
        throws IOException {
        final Header header;
        final int maxLength;
        int flags = 0;

        header = query.getHeader();
        if (header.getFlag(Flags.QR)) {
            return null;
        }
        if (header.getRcode() != Rcode.NOERROR) {
            return errorMessage(query, Rcode.FORMERR);
        }
        if (header.getOpcode() != Opcode.QUERY) {
            return errorMessage(query, Rcode.NOTIMP);
        }

        final Record queryRecord = query.getQuestion();

        final TSIGRecord queryTSIG = query.getTSIG();
        TSIG tsig = null;
        if (queryTSIG != null) {
            tsig = _tsigs.get(queryTSIG.getName());
            if (tsig == null ||
                tsig.verify(query, in, length, null) != Rcode.NOERROR) {
                return formerrMessage(in);
            }
        }

        final OPTRecord queryOPT = query.getOPT();
        if (queryOPT != null && queryOPT.getVersion() > 0) {
        }

        if (s != null) {
            maxLength = 65535;
        } else if (queryOPT != null) {
            maxLength = Math.max(queryOPT.getPayloadSize(), 512);
        } else {
            maxLength = 512;
        }

        if (queryOPT != null && (queryOPT.getFlags() & ExtendedFlags.DO) != 0) {
            flags = FLAG_DNSSECOK;
        }

        final Message response = new Message(query.getHeader().getID());
        response.getHeader().setFlag(Flags.QR);
        if (query.getHeader().getFlag(Flags.RD)) {
            response.getHeader().setFlag(Flags.RD);
        }
        response.addRecord(queryRecord, Section.QUESTION);

        final Name name = queryRecord.getName();
        final int type = queryRecord.getType();
        final int dclass = queryRecord.getDClass();
        if (type == Type.AXFR && s != null) {
            return doAXFR(name, query, tsig, queryTSIG, s);
        }
        if (!Type.isRR(type) && type != Type.ANY) {
            return errorMessage(query, Rcode.NOTIMP);
        }

        final byte rcode = addAnswer(response, name, type, dclass, 0, flags);
        if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) {
            return errorMessage(query, rcode);
        }

        addAdditional(response, flags);

        if (queryOPT != null) {
            final int optflags = (flags == FLAG_DNSSECOK) ? ExtendedFlags.DO : 0;
            final OPTRecord opt = new OPTRecord((short) 4096, rcode, (byte) 0,
                optflags);
            response.addRecord(opt, Section.ADDITIONAL);
        }

        response.setTSIG(tsig, Rcode.NOERROR, queryTSIG);
        return response.toWire(maxLength);
    }

    byte[] buildErrorMessage(Header header, int rcode, Record question) {
        final Message response = new Message();
        response.setHeader(header);
        for (int i = 0; i < 4; i++) {
            response.removeAllRecords(i);
        }
        if (rcode == Rcode.SERVFAIL) {
            response.addRecord(question, Section.QUESTION);
        }
        header.setRcode(rcode);
        return response.toWire();
    }

    public byte[] formerrMessage(byte[] in) {
        final Header header;
        try {
            header = new Header(in);
        } catch (final IOException ignored) {
            return null;
        }
        return buildErrorMessage(header, Rcode.FORMERR, null);
    }

    public byte[] errorMessage(Message query, int rcode) {
        return buildErrorMessage(query.getHeader(), rcode, query.getQuestion());
    }

    public void TCPclient(Socket s) {
        try {
            final int inLength;
            final DataInputStream dataIn;
            final DataOutputStream dataOut;
            final byte[] in;

            final InputStream is = s.getInputStream();
            dataIn = new DataInputStream(is);
            inLength = dataIn.readUnsignedShort();
            in = new byte[inLength];
            dataIn.readFully(in);

            final Message query;
            byte[] response;
            try {
                query = new Message(in);
                response = generateReply(query, in, in.length, s);
                if (response == null) {
                    return;
                }
            } catch (final IOException ignored) {
                response = formerrMessage(in);
            }
            dataOut = new DataOutputStream(s.getOutputStream());
            dataOut.writeShort(response.length);
            dataOut.write(response);
        } catch (final IOException e) {
            LOG.warn("TCPclient(" + addrport(s.getLocalAddress(), s.getLocalPort()) + ").", e);
        } finally {
            try {
                s.close();
            } catch (final IOException ignored) {}
        }
    }

    public void serveTCP(InetSocketAddress address) {
        try {
            final ServerSocket sock = new ServerSocket(address.getPort(), 128, address.getAddress());
            synchronized (_closeables) {
                _closeables.add(sock);
            }
            while (!currentThread().isInterrupted()) {
                final Socket s = accept(sock);
                final Thread thread;
                thread = new Thread(new Runnable() { @Override public void run() {
                    TCPclient(s);
                }});
                _threads.add(thread);
                thread.start();
            }
        } catch (final InterruptedIOException ignored) {
            currentThread().interrupt();
        } catch (final IOException e) {
            LOG.warn("serveTCP(" + addrport(address.getAddress(), address.getPort()) + ")", e);
        }
    }

    @Nonnull
    private Socket accept(@Nonnull ServerSocket sock) throws IOException {
        try {
            return sock.accept();
        } catch (final SocketException e) {
            if (sock.isClosed()) {
                final InterruptedIOException toThrow = new InterruptedIOException();
                toThrow.initCause(e);
                throw toThrow;
            } else {
                throw e;
            }
        }
    }

    public void serveUDP(InetSocketAddress address) {
        try {
            final DatagramSocket sock = new DatagramSocket(address.getPort(), address.getAddress());
            synchronized (_closeables) {
                _closeables.add(sock);
            }
            final short udpLength = 512;
            final byte[] in = new byte[udpLength];
            final DatagramPacket indp = new DatagramPacket(in, in.length);
            DatagramPacket outdp = null;
            while (!currentThread().isInterrupted()) {
                indp.setLength(in.length);
                receive(sock, indp);
                final Message query;
                byte[] response;
                try {
                    query = new Message(in);
                    response = generateReply(query, in,
                        indp.getLength(),
                        null);
                    if (response == null) {
                        continue;
                    }
                } catch (final IOException ignored) {
                    response = formerrMessage(in);
                }
                if (outdp == null) {
                    outdp = new DatagramPacket(response,
                        response.length,
                        indp.getAddress(),
                        indp.getPort());
                } else {
                    outdp.setData(response);
                    outdp.setLength(response.length);
                    outdp.setAddress(indp.getAddress());
                    outdp.setPort(indp.getPort());
                }
                sock.send(outdp);
            }
        } catch (final InterruptedIOException ignored) {
            currentThread().interrupt();
        } catch (final IOException e) {
            LOG.warn("serveUDP(" + addrport(address.getAddress(), address.getPort()) + ")", e);
        }
    }

    private static void receive(@Nonnull DatagramSocket sock, @Nonnull DatagramPacket indp) throws IOException {
        try {
            sock.receive(indp);
        } catch (final SocketException e) {
            if (sock.isClosed()) {
                final InterruptedIOException toThrow = new InterruptedIOException();
                toThrow.initCause(e);
                throw toThrow;
            } else {
                throw e;
            }
        }
    }

    public void addTCP(final InetSocketAddress address) {
        synchronized (this) {
            assertNotClosed();
            final Thread t;
            t = new Thread(new Runnable() { @Override public void run() {
                serveTCP(address);
            }});
            _threads.add(t);
            t.start();
        }
    }

    public void addUDP(final InetSocketAddress address) {
        synchronized (this) {
            assertNotClosed();
            final Thread t;
            t = new Thread(new Runnable() { @Override public void run() {
                serveUDP(address);
            }});
            _threads.add(t);
            t.start();
        }
    }

    protected void assertNotClosed() {
        if (_closed) {
            throw new IllegalStateException("This server was already closed.");
        }
    }

    @Override
    public void close() throws Exception {
        synchronized (this) {
            _closed = true;
            synchronized (_closeables) {
                for (final Closeable closeable : _closeables) {
                    closeQuietly(closeable);
                }
            }
            for (final Thread thread : _threads) {
                do {
                    thread.interrupt();
                    try {
                        thread.join(10);
                    } catch (final InterruptedException ignored) {
                        LOG.info("Got interrupted and could not wait for end of '" + thread + "'.");
                        currentThread().interrupt();
                    }
                } while (!currentThread().isInterrupted() && thread.isAlive());
            }
        }
    }
}


/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat Jomon, Copyright (c) 2012-2013 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jomon.cache;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;

/**
 *
A Cache based on the Least Frequently Used algorithm. If the cache is full then the element that has been accessed the fewest times will be removed.

 */
@ThreadSafe
public class LfuCache<K, V> extends InMemoryBasedCacheSupport<K, V> {

    public LfuCache(@Nonnull Class<? extends K> keyType, @Nonnull Class<? extends V> valueType) {
        super(keyType, valueType);
    }

    /**
     * Adds the new cache entry after this._last.
     */
    @Override
    protected void updateListAfterPut(CacheEntry<K, V> newEntry) {
        newEntry.setPrevious(_last);
        if (_last != null) {
            _last.setNext(newEntry);
        }
        setLast(newEntry);
    }

    /**
     * Resort the linked list. If this CacheEntry has more hits than the previous one move it up.
     */
    @Override
    protected void updateListAfterHit(CacheEntry<K, V> entry) {
        if (entry != null && !entry.equals(_first)) {
            if (entry.getHits() > entry.getPrevious().getHits()) {

                // Swap the positions
                final CacheEntry<K, V> beforePrevious = entry.getPrevious().getPrevious();
                final CacheEntry<K, V> previous = entry.getPrevious();
                final CacheEntry<K, V> next = entry.getNext();

                if (beforePrevious != null) {
                    beforePrevious.setNext(entry);
                } else {
                    _first = entry;
                }
                entry.setPrevious(beforePrevious);

                previous.setPrevious(entry);
                previous.setNext(next);

                entry.setNext(previous);

                if (next == null) {
                    setLast(previous);
                } else {
                    next.setPrevious(previous);
                }
            }
        }
    }
}


/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat Jomon, Copyright (c) 2012-2014 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jomon.runtime;

import com.google.common.base.Predicate;
import org.apache.commons.collections15.MultiMap;
import org.echocat.jomon.runtime.iterators.CloseableIterator;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillClose;
import java.util.*;

import static com.google.common.base.Predicates.equalTo;
import static java.util.Collections.*;
import static org.echocat.jomon.runtime.util.ResourceUtils.closeQuietlyIfAutoCloseable;

public class CollectionUtils {

    /**
     * Returns a the given map enriched
     * with the mappings a[0] => a[1], a[2] => a[3], ....
     * @param a the elements to construct a {@link Map} from.
     * @return a {@link Map} constructed of the specified elements.
     */
    @Nonnull
    public static <K, V> Map<K, V> putAll(@Nonnull Map<K, V> original, @Nullable Object... a) {
        if (a != null) {
            final int length = a.length;
            if (length % 2 == 1) {
                throw new IllegalArgumentException("You must provide an even number of arguments.");
            }
            for (int i = 0; i < length; i += 2) {
                // noinspection unchecked
                original.put((K) a[i], (V) a[i + 1]);
            }
        }
        return original;
    }

    @Nonnull
    public static <K, V> Map<K, V> putAll(@Nonnull Map<K, V> original, @Nullable Map<K, V> other) {
        if (other != null) {
            original.putAll(other);
        }
        return original;
    }

    /**
     * Returns a {@link LinkedHashMap}
     * with the mappings a[0] => a[1], a[2] => a[3], ....
     * @param a the elements to construct a {@link Map} from.
     * @return a {@link Map} constructed of the specified elements.
     */
    @Nonnull
    public static <K, V> Map<K, V> asMap(@Nullable Object... a) {
        return putAll(new LinkedHashMap<K, V>(), a);
    }

    /**
     * Returns a the given map enriched
     * with the mappings a[0] => a[1], a[2] => a[3], ....
     * @param a the elements to construct a {@link Map} from.
     * @return a immutable {@link Map} constructed of the specified elements.
     */
    @Nonnull
    public static <K, V> Map<K, V> putAllAndMakeImmutable(@Nonnull Map<K, V> original, @Nullable Object... a) {
        return asImmutableMap(CollectionUtils.putAll(original, a));
    }

    /**
     * Returns a {@link LinkedHashMap}
     * with the mappings a[0] => a[1], a[2] => a[3], ....
     * @param a the elements to construct a {@link Map} from.
     * @return a immutable {@link Map} constructed of the specified elements.
     */
    @Nonnull
    public static <K, V> Map<K, V> asImmutableMap(@Nullable Object... a) {
        return putAllAndMakeImmutable(new LinkedHashMap<K, V>(), a);
    }

    @Nonnull
    public static <K, V> Map<K, V> asImmutableMap(@Nullable Map<K, V> map) {
        return map != null ? unmodifiableMap(map) : Collections.<K, V>emptyMap();
    }

    public static boolean isNotEmpty(@Nullable Collection<?> collection) {
        return collection != null && !collection.isEmpty();
    }

    public static boolean isNotEmpty(@Nullable Iterable<?> iterable) {
        return iterable != null && isNotEmpty(iterable.iterator());
    }

    public static boolean isNotEmpty(@Nullable Iterator<?> iterator) {
        return iterator != null && iterator.hasNext();
    }

    public static boolean isNotEmpty(@Nullable Map<?, ?> map) {
        return map != null && !map.isEmpty();
    }

    public static boolean isNotEmpty(@Nullable MultiMap<?, ?> map) {
        return map != null && !map.isEmpty();
    }

    public static boolean isEmpty(@Nullable Collection<?> collection) {
        return collection == null || collection.isEmpty();
    }

    public static boolean isEmpty(@Nullable Iterable<?> iterable) {
        return iterable == null || isEmpty(iterable.iterator());
    }

    public static boolean isEmpty(@Nullable Iterator<?> iterator) {
        return iterator == null || !iterator.hasNext();
    }

    public static boolean isEmpty(@Nullable Map<?, ?> map) {
        return map == null || map.isEmpty();
    }

    public static boolean isEmpty(@Nullable MultiMap<?, ?> map) {
        return map == null || map.isEmpty();
    }

    @Nonnull
    public static <T, C extends Collection<T>> C addAll(@Nonnull C to, @Nullable T... elements) {
        if (elements != null) {
            Collections.addAll(to, elements);
        }
        return to;
    }

    @Nonnull
    public static <T, C extends Collection<T>> C addAll(@Nonnull C to, @Nullable Iterable<T> elements) {
        if (elements != null) {
            try {
                addAll(to, elements.iterator());
            } finally {
                closeQuietlyIfAutoCloseable(elements);
            }
        }
        return to;
    }

    @Nonnull
    public static <T, C extends Collection<T>> C addAll(@Nonnull C to, @Nullable Iterator<T> elements) {
        if (elements != null) {
            try {
                while (elements.hasNext()) {
                    to.add(elements.next());
                }
            } finally {
                closeQuietlyIfAutoCloseable(elements);
            }
        }
        return to;
    }

    /**
     * Returns a {@link Collection} containing the given objects,
     * returns an empty List, if objects is null.
     */
    @Nonnull
    public static <T> Collection<T> asCollection(@Nullable T... objects) {
        return asList(objects);
    }

    /**
     * Returns an unmodifiable {@link Collection} containing the given objects,
     * returns an empty {@link Collection}, if objects is null.
     */
    @Nonnull
    public static <T> Collection<T> asImmutableCollection(@Nullable T... objects) {
        return unmodifiableCollection(asCollection(objects));
    }

    @Nonnull
    public static <T> Collection<T> addAllAndMakeImmutable(@Nonnull Collection<T> original, @Nullable T... objects) {
        return unmodifiableCollection(addAll(original, objects));
    }

    @Nonnull
    public static <T> Collection<T> asCollection(@Nullable Iterator<T> iterator) {
        return asList(iterator);
    }

    @Nonnull
    public static <T> Collection<T> asImmutableCollection(@Nullable Iterator<T> iterator) {
        return unmodifiableCollection(asCollection(iterator));
    }

    @Nonnull
    public static <T> Collection<T> addAllAndMakeImmutable(@Nonnull Collection<T> original, @Nullable Iterator<T> iterator) {
        return unmodifiableCollection(addAll(original, iterator));
    }

    @Nonnull
    public static <T> Collection<T> asCollection(@Nullable Iterable<T> in) {
        final Collection<T> result;
        if (in instanceof Collection) {
            result = (Collection<T>) in;
        } else {
            result = asList(in);
        }
        return result;
    }

    @Nonnull
    public static <T> Collection<T> asImmutableCollection(@Nullable Iterable<T> in) {
        return unmodifiableCollection(asCollection(in));
    }

    @Nonnull
    public static <T> Collection<T> addAllAndMakeImmutable(@Nonnull Collection<T> original, @Nullable Iterable<T> in) {
        return unmodifiableCollection(addAll(original, in));
    }

    /**
     * Returns a {@link List} containing the given objects,
     * returns an empty List, if objects is null.
     */
    @Nonnull
    public static <T> List<T> asList(@Nullable T... objects) {
        final List<T> result;
        if (objects == null) {
            result = new ArrayList<>();
        } else {
            final int initialCapacity = Math.max(16, ((objects.length + 2) / 3) * 4);
            result = new ArrayList<>(initialCapacity);
            result.addAll(new ArrayWrapper<>(objects));
        }
        return result;
    }

    /**
     * Returns an unmodifiable {@link List} containing the given objects,
     * returns an empty {@link List}, if objects is null.
     */
    @Nonnull
    public static <T> List<T> asImmutableList(@Nullable T... objects) {
        return unmodifiableList(asList(objects));
    }

    @Nonnull
    public static <T> List<T> addAllAndMakeImmutable(@Nonnull List<T> original, @Nullable T... objects) {
        return unmodifiableList(addAll(original, objects));
    }

    @Nonnull
    public static <T> List<T> asList(@Nullable Iterator<T> iterator) {
        final List<T> result = new ArrayList<>();
        try {
            if (iterator != null) {
                while (iterator.hasNext()) {
                    result.add(iterator.next());
                }
            }
        } finally {
            closeQuietlyIfAutoCloseable(iterator);
        }
        return result;
    }

    @Nonnull
    public static <T> List<T> asImmutableList(@Nullable Iterator<T> iterator) {
        return unmodifiableList(asList(iterator));
    }

    @Nonnull
    public static <T> List<T> addAllAndMakeImmutable(@Nonnull List<T> original, @Nullable Iterator<T> iterator) {
        return unmodifiableList(addAll(original, iterator));
    }

    @Nonnull
    public static <T> List<T> asList(@Nullable Iterable<T> in) {
        final List<T> result;
        if (in instanceof List) {
            result = (List<T>) in;
        } else if (in instanceof Collection) {
            result = new ArrayList<>((Collection<T>) in);
        } else {
            result = new ArrayList<>();
            addAll(result, in);
        }
        return result;
    }

    @Nonnull
    public static <T> List<T> asImmutableList(@Nullable Iterable<T> in) {
        return unmodifiableList(asList(in));
    }

    @Nonnull
    public static <T> List<T> addAllAndMakeImmutable(@Nonnull List<T> original, @Nullable Iterable<T> in) {
        return unmodifiableList(addAll(original, in));
    }

    /**
     * Returns a {@link Set} containing the given objects,
     * returns an empty Set, if objects is null.
     */
    @Nonnull
    public static <T> Set<T> asSet(@Nullable T... objects) {
        final Set<T> result;
        if (objects == null) {
            result = new LinkedHashSet<>();
        } else {
            final int initialCapacity = Math.max(16, ((objects.length + 2) / 3) * 4);
            result = new LinkedHashSet<>(initialCapacity);
            result.addAll(new ArrayWrapper<>(objects));
        }
        return result;
    }

    /**
     * Returns an unmodifiable {@link Set} containing the given objects,
     * returns an empty Set, if objects is null.
     */
    @Nonnull
    public static <T> Set<T> asImmutableSet(@Nullable T... objects) {
        return addAllAndMakeImmutable(new HashSet<T>(), objects);
    }

    /**
     * Returns an unmodifiable {@link Set} containing the given objects.
     */
    @Nonnull
    public static <T> Set<T> addAllAndMakeImmutable(@Nonnull Set<T> original, @Nullable T... objects) {
        return unmodifiableSet(addAll(original, objects));
    }

    @Nonnull
    public static <T> Set<T> asSet(@Nullable Iterator<T> iterator) {
        return addAll(new LinkedHashSet<T>(), iterator);
    }

    @Nonnull
    public static <T> Set<T> asImmutableSet(@Nullable Iterator<T> iterator) {
        return unmodifiableSet(asSet(iterator));
    }

    @Nonnull
    public static <T> Set<T> addAllAndMakeImmutable(@Nonnull Set<T> original, @Nullable Iterator<T> iterator) {
        return unmodifiableSet(addAll(original, iterator));
    }

    @Nonnull
    public static <T> Set<T> asSet(@Nullable Iterable<T> in) {
        final Set<T> result;
        if (in instanceof Set) {
            result = (Set<T>) in;
        } else if (in instanceof Collection) {
            result = new LinkedHashSet<>((Collection<T>) in);
        } else {
            result = addAll(new LinkedHashSet<T>(), in);
        }
        return result;
    }

    @Nonnull
    public static <T> Set<T> asImmutableSet(@Nullable Iterable<T> in) {
        return unmodifiableSet(asSet(in));
    }

    @Nonnull
    public static <T> Set<T> addAllAndMakeImmutable(@Nonnull Set<T> original, @Nullable Iterable<T> in) {
        return unmodifiableSet(addAll(original, in));
    }

    @Nonnull
    public static <T> CloseableIterator<T> asSingletonIterator(@Nullable final T element) {
        return new CloseableIterator<T>() {

            private boolean _elementFetched;

            @Override
            public boolean hasNext() {
                return !_elementFetched;
            }

            @Override
            public T next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                _elementFetched = true;
                return element;
            }

            @Override public void remove() { throw new UnsupportedOperationException(); }
            @Override public void close() {}
        };
    }

    @Nonnull
    public static <T> CloseableIterator<T> asCloseableIterator(@Nullable final Iterator<T> iterator) {
        final CloseableIterator<T> result;
        if (iterator instanceof CloseableIterator) {
            result = (CloseableIterator<T>) iterator;
        } else if (iterator != null) {
            result = new CloseableIterator<T>() {
                @Override public void close() {}
                @Override public boolean hasNext() { return  iterator.hasNext(); }
                @Override public T next() { return iterator.next(); }
                @Override public void remove() { iterator.remove(); }
            };
        } else {
            result = emptyIterator();
        }
        return result;
    }

    @SafeVarargs
    @Nonnull
    public static <T> CloseableIterator<T> asIterator(@Nullable T... elements) {
        final CloseableIterator<T> result;
        if (elements != null) {
            final int length = elements.length;
            if (length == 0) {
                result = emptyIterator();
            } else if (length == 1) {
                result = asSingletonIterator(elements[0]);
            } else {
                result = asIterator(Arrays.asList(elements));
            }
        } else {
            result = emptyIterator();
        }
        return result;
    }

    @Nonnull
    public static <T> CloseableIterator<T> asIterator(@Nullable Iterable<T> elements) {
        final CloseableIterator<T> result;
        if (elements != null) {
            final Iterator<T> iterator = elements.iterator();
            if (iterator.hasNext()) {
                result = asCloseableIterator(iterator);
            } else {
                result = emptyIterator();
            }
        } else {
            result = emptyIterator();
        }
        return result;
    }

    @Nonnull
    public static <T> CloseableIterator<T> emptyIterator() {
        return new CloseableIterator<T>() {
            @Override public boolean hasNext() { return false; }
            @Override public T next() { throw new NoSuchElementException(); }
            @Override public void remove() { throw new UnsupportedOperationException(); }
            @Override public void close() {}
        };
    }

    public static <T> boolean oneApplies(@Nullable @WillClose Iterable<T> elements, @Nullable T predicate) {
        return elements != null && oneApplies(elements, equalTo(predicate));
    }

    public static <T> boolean oneApplies(@Nullable @WillClose Iterable<T> elements, @Nonnull Predicate<T> predicate) {
        return elements != null && oneApplies(elements.iterator(), predicate);
    }

    public static <T> boolean oneApplies(@Nullable @WillClose Iterable<T> elements, @Nullable final Collection<T> predicate) {
        return oneApplies(elements, new Predicate<T>() { @Override public boolean apply(@Nullable T input) {
            return predicate == null || predicate.contains(input);
        }});
    }

    public static <T> boolean oneApplies(@Nullable @WillClose Iterator<T> elements, @Nonnull T predicate) {
        return elements != null && oneApplies(elements, equalTo(predicate));
    }

    public static <T> boolean oneApplies(@Nullable @WillClose Iterator<T> elements, @Nonnull Predicate<T> predicate) {
        boolean result = false;
        if (elements != null) {
            try {
                while (elements.hasNext() && !result) {
                    result = predicate.apply(elements.next());
                }
            } finally {
                closeQuietlyIfAutoCloseable(elements);
            }
        }
        return result;
    }

    @Nonnegative
    public static <T> long countElementsOf(@Nullable Iterable<T> elements) {
        return countElementsOf(elements, null);
    }

    @Nonnegative
    public static <T> long countElementsOf(@Nullable Iterable<T> elements, @Nullable Predicate<T> matches) {
        return elements instanceof Collection && matches == null ? ((Collection) elements).size() : countElementsOf(elements.iterator(), matches);
    }

    @Nonnegative
    public static <T> long countElementsOf(@Nullable Iterator<T> iterator) {
        return countElementsOf(iterator, null);
    }

    @Nonnegative
    public static <T> long countElementsOf(@Nullable Iterator<T> iterator, @Nullable Predicate<T> matches) {
        long count = 0;
        try {
            if (iterator != null) {
                while (iterator.hasNext()) {
                    final T what = iterator.next();
                    if (matches == null || matches.apply(what)) {
                        count++;
                    }
                }
            }
        } finally {
            closeQuietlyIfAutoCloseable(iterator);
        }
        return count;
    }

    @Nullable
    public static <T> T findFirstOf(@Nullable Iterable<T> elements) {
        return elements != null ? findFirstOf(elements.iterator()) : null;
    }

    @Nullable
    public static <T> T findFirstOf(@Nullable Iterator<T> iterator) {
        T firstElement = null;
        try {
            if (iterator != null && iterator.hasNext()) {
                firstElement = iterator.next();
            }
        } finally {
            closeQuietlyIfAutoCloseable(iterator);
        }
        return firstElement;
    }

    @Nonnegative
    public static <T> long removeAllOf(@Nullable Iterable<T> elements) {
        return removeAllOf(elements, null);
    }

    @Nonnegative
    public static <T> long removeAllOf(@Nullable Iterable<T> elements, @Nullable Predicate<T> matching) {
        return removeAllOf(elements.iterator(), matching);
    }

    @Nonnegative
    public static <T> long removeAllOf(@Nullable Iterator<T> iterator) {
        return removeAllOf(iterator, null);
    }

    @Nonnegative
    public static <T> long removeAllOf(@Nullable Iterator<T> iterator, @Nullable Predicate<T> matching) {
        long removed = 0;
        try {
            if (iterator != null) {
                while (iterator.hasNext()) {
                    final T what = iterator.next();
                    if (matching == null || matching.apply(what)) {
                        iterator.remove();
                        removed++;
                    }
                }
            }
        } finally {
            closeQuietlyIfAutoCloseable(iterator);
        }
        return removed;
    }

    /**
     * This class is similar to the class which is returned by {@link Arrays#asList}
     * but does not clone the wrapped array if {@link #toArray} is called.
     */
    private static class ArrayWrapper<E> extends AbstractList<E> {
        private final E[] _wrappedArray;

        private ArrayWrapper(E[] arrayToWrap) {
            _wrappedArray = arrayToWrap;
        }

        @Override
        public E get(int index) {
            return _wrappedArray[index];
        }

        @Override
        public int size() {
            return _wrappedArray.length;
        }

        /**
         * Returns the wrapped array.
         */
        @Override
        public Object[] toArray() {
            return _wrappedArray;
        }
    }

    private CollectionUtils() {}
}


/*****************************************************************************************
 * *** BEGIN LICENSE BLOCK *****
 *
 * Version: MPL 2.0
 *
 * echocat Jomon, Copyright (c) 2012-2013 echocat
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * *** END LICENSE BLOCK *****
 ****************************************************************************************/

package org.echocat.jomon.runtime;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

public class StringUtils extends org.apache.commons.lang3.StringUtils {

    protected StringUtils() {}

    /**
     * Count the number of occurrences of sub in s
     */
    @Nonnegative
    public static int countSubString(@Nonnull String string, @Nonnull String sub) {
        int count = 0;
        int pos = 0;
        do {
            pos = string.indexOf(sub, pos);
            if (pos != -1) {
                count++;
                pos += sub.length();
            }
        } while (pos != -1);
        return count;
    }

    /**
     * Split a string and return the values as an array.
     *
     * @param delimiters delimiters that are accepted by StringTokenizer
     */
    @Nonnull
    public static List<String> splitToList(@Nonnull String source, @Nonnull String delimiters, boolean returnDelimiters, boolean trim) {
        final List<String> values = new ArrayList<>();
        if (source != null) {
            final StringTokenizer st = new StringTokenizer(source, delimiters, returnDelimiters);
            while (st.hasMoreTokens()) {
                String value = st.nextToken();
                if (trim) {
                    value = trim(value);
                }
                values.add(value);
            }
        }
        return values;
    }

    /**
     * Split a string and return the values as an array.
     *
     * @param delimiters delimiters that are accepted by StringTokenizer
     */
    @Nonnull
    public static String[] split(@Nonnull String source, @Nonnull String delimiters, boolean returnDelimiters, boolean trim) {
        final List<String> values = splitToList(source, delimiters, returnDelimiters, trim);
        final String[] result = new String[values.size()];
        values.toArray(result);
        return result;
    }

    @Nonnull
    public static String[] split(@Nonnull String source, @Nonnull String delimiters, boolean returnDelimiters) {
        return split(source, delimiters, returnDelimiters, false);
    }

    /**
     * Appends the separator string, if <code>{@link StringBuilder#length() buf.lenght()} > 0</code> and then appends the element.
     */
    public static void addElement(@Nonnull StringBuilder buf, @Nonnull String separator, @Nonnull String element) {
        if (buf.length() > 0) {
            buf.append(separator);
        }
        buf.append(element);
    }

    /**
     * Convert a string to lower case. (Faster than String.toLowerCase)
     */
    public static void toLowerCase(char[] chars) {
        for (int a = 0; a < chars.length; a++) {
            chars[a] = Character.toLowerCase(chars[a]);
        }
    }

    /**
     * Test whether 'find' can be found at position 'startPos' in the string 'src'.
     */
    public static boolean startsWith(char[] src, char[] find, int startAt) {
        int startPos = startAt;
        boolean result = true;

        // Check ranges
        if (src.length < startPos + find.length) {
            result = false;
        } else {
            final int max = find.length;
            for (int a = 0; a < max && result; a++) {
                if (src[startPos] != find[a]) {
                    result = false;
                }
                startPos++;
            }
        }
        return result;
    }

    public static boolean containsNoneOf(CharSequence stringToSearch, CharSequence... stringsToSearchFor) {
        return indexOfAny(stringToSearch, stringsToSearchFor) == -1;
    }

    public static boolean containsAnyOf(CharSequence stringToSearch, CharSequence... stringsToSearchFor) {
        return indexOfAny(stringToSearch, stringsToSearchFor) > -1;
    }

    public static boolean containsAny(CharSequence charSequence, List<Character> searchChars) {
        final char[] charArray = new char[searchChars.size()];
        for (int i = 0; i < searchChars.size(); i++) {
            charArray[i] = searchChars.get(i);
        }
        return containsAny(charSequence, charArray);
    }

    /**
     * Same as String.indexOf but (slightly) faster ;)
     *
     * Hint: Performance optimized code - so some warnings are suppressed.
     */
    @SuppressWarnings({"MethodWithMultipleReturnPoints", "LabeledStatement", "ValueOfIncrementOrDecrementUsed", "ContinueStatement", "ContinueStatementWithLabel"})
    public static int indexOf(char[] src, char[] find, int startAt) {
        int startPos = startAt;
        final int max = src.length - find.length;
        if (startPos > max) {
            return -1;
        }
        final char find0 = find[0];
        final int len = find.length;
        int j;
        int k;

        // Find the first character
        startOver:
        while (startPos <= max) {
            if (src[startPos++] == find0) {
                // First character found - look for the rest
                j = startPos;
                k = 1;
                while (k < len) {
                    if (src[j++] != find[k++]) {
                        continue startOver;
                    }
                }
                return startPos - 1;
            }
        }
        return -1;
    }
}