/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2011 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 *
 * Contributor(s):
 *
 * Portions Copyrighted 2011 Sun Microsystems, Inc.
 */
package org.netbeans.spi.whitelist.support;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.SourceUtils;
import org.netbeans.api.whitelist.WhiteListQuery;
import org.netbeans.spi.whitelist.WhiteListQueryImplementation;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.openide.util.Union2;

/**
 *
 * @author Tomas Zezula
 */
//@NotThreadSafe
/*public - needs clean up*/
final class WhiteListImplementationBuilder {

    private static final byte INVOKE = 1;
    private static final byte OVERRIDE = INVOKE << 1;

    private Model model;

    private WhiteListImplementationBuilder() {
        this.model = new Model();
    }

    @NonNull
    public WhiteListImplementationBuilder setDisplayName(@NonNull final String displayName) {
        Parameters.notNull("displayName", displayName); //NOI18N
        checkPreconditions();
        model.setDisplayName(displayName);
        return this;
    }

    @NonNull
    public WhiteListImplementationBuilder addCheckedPackage(@NonNull final String checkedPackage) {
        Parameters.notNull("checkedPackage", checkedPackage);   //NOI18N
        checkPreconditions();
        model.addCheckedPackage(checkedPackage);
        return this;
    }

    @NonNull
    public WhiteListImplementationBuilder addInvocableClass(@NonNull final String classBinaryName) {
        Parameters.notNull("classBinaryName", classBinaryName); //NOI18N
        checkPreconditions();
        model.addClass(classBinaryName, INVOKE);
        return this;
    }

    @NonNull
    public WhiteListImplementationBuilder addInvocableMethod(
            @NonNull final String classBinaryName,
            @NonNull final String methodName,
            @NonNull final String... argumentTypes) {
        Parameters.notNull("classBinaryName", classBinaryName);    //NOI18N
        Parameters.notNull("methodName", methodName);   //NOI18N
        Parameters.notNull("argumentTypes", argumentTypes); //NOI18N
        checkPreconditions();
        model.addMethod(classBinaryName, methodName, argumentTypes, INVOKE);
        return this;
    }

    @NonNull
    public WhiteListImplementationBuilder addSubclassableClass(@NonNull final String classBinaryName) {
        Parameters.notNull("classBinaryName", classBinaryName); //NOI18N
        checkPreconditions();
        model.addClass(classBinaryName, OVERRIDE);
        return this;
    }

    @NonNull
    public WhiteListImplementationBuilder addOverridableMethod(
            @NonNull final String classBinaryName,
            @NonNull final String methodName,
            @NonNull final String... argumentTypes) {
        Parameters.notNull("classBinaryName", classBinaryName);    //NOI18N
        Parameters.notNull("methodName", methodName);   //NOI18N
        Parameters.notNull("argumentTypes", argumentTypes); //NOI18N
        checkPreconditions();
        model.addMethod(classBinaryName, methodName, argumentTypes, OVERRIDE);
        return this;
    }

    @NonNull
    public WhiteListImplementationBuilder addAllowedPackage(@NonNull final String packageName, boolean includingSubpackages) {
        Parameters.notNull("checkedPackage", packageName);   //NOI18N
        checkPreconditions();
        model.addAllowedPackage(includingSubpackages ? packageName + "." : packageName);
        return this;
    }

    @NonNull
    public WhiteListImplementationBuilder addDisallowedPackage(@NonNull final String packageName, boolean includingSubpackages) {
        Parameters.notNull("checkedPackage", packageName);   //NOI18N
        checkPreconditions();
        model.addDisallowedPackage(includingSubpackages ? packageName + "." : packageName);
        return this;
    }

    @NonNull
    public WhiteListQueryImplementation.WhiteListImplementation build() {
        final WhiteList result = new WhiteList(model.build());
        model = null;
        return result;
    }

    @NonNull
    public static WhiteListImplementationBuilder create() {
        return new WhiteListImplementationBuilder();
    }

    //<editor-fold defaultstate="collapsed" desc="Private implementation">
    private void checkPreconditions() {
        if (model == null) {
            throw new IllegalStateException("Modifying already built builder, create a new one");   //NOI18N
        }
    }

    private static final class WhiteList implements WhiteListQueryImplementation.WhiteListImplementation {

        private final Model model;

        private WhiteList(@NonNull final Model model) {
            assert model != null;
            this.model = model;
        }

        @Override
        @NbBundle.Messages({
            "RULE_Class=Class Access",
            "RULE_Meth=Method Invocation",
            "RULE_Cons=Method Invocation",
            "DESC_Class=Access of type {0} is forbidden by {1}.",
            "DESC_Meth=Invocation of method {0} is forbidden by {1}.",
            "DESC_Cons=Invocation of constuctor {0} is forbidden by {1}."
        })
        public WhiteListQuery.Result check(
                @NonNull final ElementHandle<?> element,
                @NonNull final WhiteListQuery.Operation operation) {
            assert element != null;
            assert operation != null;
            boolean b = model.isAllowed(element,INVOKE);
            String ruleName = null;
            String ruleDesc = null;
            if (!b) {
                if (element.getKind().isClass() || element.getKind().isInterface()) {
                    ruleName = Bundle.RULE_Class();
                    ruleDesc = Bundle.DESC_Class(displayName(element), model.getDisplayName());
                } else if (element.getKind() == ElementKind.CONSTRUCTOR) {
                    ruleName = Bundle.RULE_Cons();
                    ruleDesc = Bundle.DESC_Cons(displayName(element), model.getDisplayName());
                } else {
                    ruleName = Bundle.RULE_Meth();
                    ruleDesc = Bundle.DESC_Meth(displayName(element), model.getDisplayName());
                }
                return new WhiteListQuery.Result(
                        // TODO: whitelist ID should be configurable via a setter method:
                        Collections.singletonList(new WhiteListQuery.RuleDescription(ruleName, ruleDesc, null))
                        );
            } else {
                return new WhiteListQuery.Result();
            }
        }

        @Override
        public void addChangeListener(@NonNull final ChangeListener listener) {
            //immutable no change support
        }

        @Override
        public void removeChangeListener(@NonNull final ChangeListener listener) {
            //immutable no change support
        }

        @NonNull
        private static String displayName(
            @NonNull final ElementHandle<? extends Element> handle) {
            final ElementKind kind = handle.getKind();
            final String[] vmSig = SourceUtils.getJVMSignature(handle);
            if (kind.isClass() || kind.isInterface()) {
                assert vmSig.length == 1;
                return cannonicalName(vmSig[0]);
            } else if (kind == ElementKind.CONSTRUCTOR) {
                assert vmSig.length == 3;
                final StringBuilder sb = new StringBuilder();
                int index = vmSig[2].lastIndexOf(')');
                assert index > 0;
                sb.append(simpleName(vmSig[0]));
                appendParams(vmSig[2],index,sb);
                return sb.toString();
            } else if (kind == ElementKind.METHOD) {
                assert vmSig.length == 3;
                final StringBuilder sb = new StringBuilder();
                int index = vmSig[2].lastIndexOf(')');
                assert index > 0;
                cannonicalName(vmSig[2].substring(index+1),sb);
                sb.append(' '); //NOI18N
                sb.append(vmSig[1]);
                appendParams(vmSig[2],index,sb);
                return sb.toString();
            } else  {
                throw new UnsupportedOperationException(kind.name());
            }
        }

        private static void appendParams(
                @NonNull final String methodSig,
                @NonNull final int paramsEndIndex,
                @NonNull final StringBuilder sb) {
            sb.append('('); //NOI18N
                int state = 0;
                boolean first = true;
                for (int i=1 ,j = 1; j< paramsEndIndex;) {
                    final char la = methodSig.charAt(j);
                    switch (state) {
                        case 0:
                            if (la=='L') {      //NOI18N
                                state = 1;
                                j++;
                            }
                            else if (la=='[') { //NOI18N
                                j++;
                            } else {
                                j++;
                                if (first) {
                                    first = false;
                                } else {
                                    sb.append(", ");    //NOI18N
                                }
                                cannonicalName(methodSig.substring(i,j), sb);
                                i=j;
                            }
                            break;
                        case 1:
                            if (la==';') {      //NOI18N
                                j++;
                                if (first) {
                                    first = false;
                                } else {
                                    sb.append(", ");    //NOI18N
                                }
                                cannonicalName(methodSig.substring(i,j), sb);
                                i=j;
                                state = 0;
                            } else {
                                j++;
                            }
                            break;
                        default:
                            throw new IllegalStateException();
                    }
                }
                sb.append(')'); //NOI18N
        }

        @NonNull
        private static String cannonicalName(@NonNull final String binaryName) {
            return binaryName.replace('$', '.').replace('/', '.');    //NOI18N
        }

        @NonNull
        private static String simpleName(@NonNull final String binaryName) {
            final int index =  Math.max(binaryName.lastIndexOf('.'),binaryName.lastIndexOf('$'));
            return index == -1 ? binaryName : binaryName.substring(index+1);
        }

        private static void cannonicalName(
                @NonNull final String type,
                @NonNull final StringBuilder into) {
            final char la = type.charAt(0);
            switch (la) {
                case 'V':                       //NOI18N
                    into.append("void");        //NOI18N
                    break;
                case 'Z':                       //NOI18N
                    into.append("boolean");     //NOI18N
                    break;
                case 'B':                       //NOI18N
                    into.append("byte");        //NOI18N
                    break;
                case 'S':                       //NOI18N
                    into.append("short");       //NOI18N
                    break;
                case 'I':                       //NOI18N
                    into.append("int");         //NOI18N
                    break;
                case 'J':                       //NOI18N
                    into.append("long");        //NOI18N
                    break;
                case 'C':                       //NOI18N
                    into.append("char");        //NOI18N
                    break;
                case 'F':                       //NOI18N
                    into.append("float");       //NOI18N
                    break;
                case 'D':                       //NOI18N
                    into.append("double");      //NOI18N
                    break;
                case 'L':                       //NOI18N
                    into.append(cannonicalName(type.substring(1,type.length()-1)));
                    break;
                case '[':                       //NOI18N
                    cannonicalName(type.substring(1), into);
                    into.append("[]");          //NOI18N
                    break;
                default:
                    throw new IllegalStateException();
            }
        }
    }

    private static final class Model {
        private static final String DEF_NAMES = PackedNames.class.getName();
        private String whiteListName;
        private Union2<StringBuilder,Pattern> checkedPkgs;
        private List<String> allowedPackages; // silly: if package ends with dot then it handles subpackages as well
        private List<String> disallowedPackages; // silly: if package ends with dot then it handles subpackages as well
        private final Names names;
        private final IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<CacheNode>>>> root =
                new IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<CacheNode>>>>();

        private Model() {
                try {
                    names  = (Names) Class.forName(System.getProperty("WhiteListBuilder.names", DEF_NAMES)).newInstance();  //NOI18N
                } catch (InstantiationException ex) {
                    throw new IllegalStateException("Cannot instantiate names", ex);    //NOI18N
                } catch (IllegalAccessException ex) {
                    throw new IllegalStateException("Cannot instantiate names", ex);    //NOI18N
                } catch (ClassNotFoundException ex) {
                    throw new IllegalStateException("Cannot instantiate names", ex);    //NOI18N
                }
                checkedPkgs = Union2.<StringBuilder,Pattern>createFirst(new StringBuilder());
                allowedPackages = new ArrayList<String>();
                disallowedPackages = new ArrayList<String>();
        }

        void addCheckedPackage(
                @NonNull final String pkg) {
            if (checkedPkgs.first().length()>0) {
                checkedPkgs.first().append('|');    //NOI18N
            }
            checkedPkgs.first().append(Pattern.quote(pkg+'.')).append(".*"); //NOI18N
        }

        void addAllowedPackage(
                @NonNull final String pkg) {
            allowedPackages.add(pkg);
        }
        
        void addDisallowedPackage(
                @NonNull final String pkg) {
            disallowedPackages.add(pkg);
        }
        
        void addClass(
                @NonNull final String binaryName,
                final byte mode) {
            final String[] pkgNamePair = splitName(binaryName,'/');
            final Integer pkgId = names.putName(folderToPackage(pkgNamePair[0]));
            @SuppressWarnings("RedundantStringConstructorCall")
            final Integer clsId = names.putName(new String(pkgNamePair[1]));
            final IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<CacheNode>>> pkgNode =
                root.putIfAbsent(pkgId, new IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<CacheNode>>>());
            final IntermediateCacheNode<IntermediateCacheNode<CacheNode>> clsNode =
                pkgNode.putIfAbsent(clsId, new IntermediateCacheNode<IntermediateCacheNode<CacheNode>>());
            clsNode.state |= mode;
        }

        void addMethod(
                @NonNull final String clsBinaryName,
                @NonNull final String methodName,
                @NonNull final String[] argTypes,
                @NonNull final byte mode) {
            final String[] pkgNamePair = splitName(clsBinaryName,'/');
            final Integer pkgId = names.putName(folderToPackage(pkgNamePair[0]));
            @SuppressWarnings("RedundantStringConstructorCall")
            final Integer clsId = names.putName(new String(pkgNamePair[1]));
            final Integer methodNameId = names.putName(methodName);
            final Integer metodSigId = names.putName(vmSignature(argTypes));
            final IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<CacheNode>>> pkgNode =
                root.putIfAbsent(pkgId, new IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<CacheNode>>>());
            final IntermediateCacheNode<IntermediateCacheNode<CacheNode>> clsNode =
                pkgNode.putIfAbsent(clsId, new IntermediateCacheNode<IntermediateCacheNode<CacheNode>>());
            final IntermediateCacheNode<CacheNode> methodNameNode =
                clsNode.putIfAbsent(methodNameId, new IntermediateCacheNode<CacheNode>());
            final CacheNode methodSigNode =
                methodNameNode.putIfAbsent(metodSigId, new CacheNode());
            methodSigNode.state |= mode;
        }

        void setDisplayName(final String name) {
            whiteListName = name;
        }

        @NbBundle.Messages({
            "TXT_UnknownWhiteList=Unknown WhiteList"
        })
        Model build() {
            checkedPkgs = Union2.createSecond(
                    Pattern.compile(checkedPkgs.first().toString()));
            if (whiteListName == null) {
                whiteListName = Bundle.TXT_UnknownWhiteList();
            }
            return this;
        }

        private boolean isThere(List<String> packages, String pkg) {
            String pkg2 = pkg+"."; // silly
            for (String s : packages) {
                if (pkg2.startsWith(s)) {
                    // if subpackages are handled then all is OK
                    if (s.endsWith(".")) {
                        return true;
                    } else if (pkg.equals(s)) {
                        return true;
                    }
                }
            }
            return false;
        }
        
        boolean isAllowed(
                @NonNull final ElementHandle<?> element,
                final byte mode) {
            final String[] vmSignatures = SourceUtils.getJVMSignature(element);
            final String[] pkgNamePair = splitName(vmSignatures[0],'.');  //NOI18N
            
            if (isThere(allowedPackages, pkgNamePair[0])) {
                return true;
            }
            if (isThere(disallowedPackages, pkgNamePair[0])) {
                return false;
            }
            
            if (!checkedPkgs.second().matcher(pkgNamePair[0]+'.').matches()) {  //NOI18N
                return true;
            }
            final Integer pkgId = names.getName(pkgNamePair[0]);
            final Integer clsId = names.getName(pkgNamePair[1]);
            final IntermediateCacheNode<IntermediateCacheNode<IntermediateCacheNode<CacheNode>>> pkgNode = root.get(pkgId);
            if (pkgNode == null) {
                return false;
            }
            final IntermediateCacheNode<IntermediateCacheNode<CacheNode>> clsNode = pkgNode.get(clsId);
            if (clsNode == null) {
                return false;
            }
            if ((clsNode.state & mode) == mode) {
                return true;
            }
            if (element.getKind() == ElementKind.METHOD ||
                element.getKind() == ElementKind.CONSTRUCTOR) {
                final Integer methodNameId = names.getName(vmSignatures[1]);
                final Integer methodSigId = names.getName(paramsOnly(vmSignatures[2]));
                final IntermediateCacheNode<CacheNode> methodNameNode = clsNode.get(methodNameId);
                if (methodNameNode == null) {
                    return false;
                }
                final CacheNode methodSigNode = methodNameNode.get(methodSigId);
                if (methodSigNode == null) {
                    return false;
                }
                return (methodSigNode.state & mode) == mode;
            } else if ((element.getKind().isClass() ||
                element.getKind().isInterface()) && clsNode.hasChildren()) {
                //If the request is for type and it has at least one alowed method
                //allow it. It would be strange to black list type usage which method is allowed
                return true;
            }
            return false;
        }

        String getDisplayName() {
            return whiteListName;
        }

        private static class CacheNode {
            byte state;
        }

        private static class IntermediateCacheNode<T extends CacheNode> extends CacheNode {
            private Map<Integer,T> nodes = new HashMap<Integer, T>();

            @NonNull
            final T putIfAbsent (
                    @NonNull Integer id,
                    @NonNull T node) {
                assert id != null;
                assert node != null;
                T result = nodes.get(id);
                if (result == null) {
                    result = node;
                    nodes.put (id, result);
                }
                return result;
            }

            @CheckForNull
            final T get(@NullAllowed Integer id) {
                return id == null ? null : nodes.get(id);
            }

            final boolean hasChildren() {
                return !nodes.isEmpty();
            }
        }

        @NonNull
        private String[] splitName(
                @NonNull final String qName,
                final char separator) {
            int index = qName.lastIndexOf(separator);    //NOI18N
            String pkg, name;
            if (index == -1) {
                pkg = "";   //NOI18N
                name = qName;
            } else {
                pkg = qName.substring(0, index);
                name = qName.substring(index+1);
            }
            return new String[] {pkg, name};
        }

        @NonNull
        private String paramsOnly(
                @NonNull final String name) {
            assert name.charAt(0) == '(';   //NOI18N;
            int index = name.lastIndexOf(')');  //NOI18N
            assert index > 0;
            return name.substring(1, index);
        }

        @NonNull
        private String vmSignature(
                @NonNull final String[] types) {
            final StringBuilder sb = new StringBuilder();
            for (String type : types) {
                encodeType(type,sb);
            }
            return sb.toString();
        }

        private void encodeType(
                @NonNull final String type,
                @NonNull StringBuilder sb) {
            assert type != null;
            assert sb != null;
            if ("void".equals(type)) {  //NOI18N
                sb.append('V');	    // NOI18N
            } else if ("boolean".equals(type)) {    //NOI18N
                sb.append('Z');	    // NOI18N
            } else if ("byte".equals(type)) {   //NOI18N
                sb.append('B');     //NOI18N
            } else if ("short".equals(type)) {    //NOI18N
                sb.append('S');	    // NOI18N
            } else if ("int".equals(type)) { //NOI18N
                sb.append('I');	    // NOI18N
            } else if ("long".equals(type)) {
                sb.append('J');	    // NOI18N
            } else if ("char".equals(type)) { //NOI18N
                sb.append('C');	    // NOI18N
            } else if ("float".equals(type)) {  //NOI18N
                sb.append('F');	    // NOI18N
            } else if ("double".equals(type)) {
                sb.append('D');	    // NOI18N
            } else if (type.charAt(type.length()-1) == ']') {   //NOI18N
                sb.append('[');
                encodeType(type.substring(0,type.length()-2), sb);
            } else {
                sb.append('L'); //NOI18N
                sb.append(type);
                sb.append(';'); //NOI18N
            }
        }

        @NonNull
        private static String folderToPackage(@NonNull final String folder) {
            return folder.replace( '/', '.' );  //NOI18N
        }
    }

    private static interface Names {
        @NonNull
        Integer putName(@NonNull String name);

        @CheckForNull
        Integer getName(@NonNull String name);
    }

    //@NotThreadSafe
    static class SimpleNames implements Names {

        private final Map<String,Integer> names = new HashMap<String, Integer>();
        private int counter = Integer.MIN_VALUE;

        @Override
        @NonNull
        public Integer putName(@NonNull final String name) {
            assert name != null;
            Integer result = names.get(name);
            if (result == null) {
                result = counter++;
                names.put(name, result);
            }
            return result;
        }

        @Override
        @CheckForNull
        public Integer getName(@NonNull final String name) {
            assert name != null;
            return names.get(name);
        }
    }

    //@NotThreadSafe
    static class PackedNames implements Names {

        private static final int DEF_SLOTS = 1<<10;
        private static final int DEF_INIT_BYTES = 1<<16;

        private final int hashMask;
        private Entry[] slots;
        private byte[] storage;
        private int pos;

        PackedNames() {
            slots = new Entry[DEF_SLOTS];
            hashMask = slots.length - 1;
            storage = new byte[DEF_INIT_BYTES];
        }

        @Override
        public Integer putName(@NonNull final String name) {
            assert name != null;
            final int hc = name.hashCode() & hashMask;
            final byte[] sbytes = decode(name);
            Entry entry = slots[hc];
            while (entry != null && !contentEquals(entry,sbytes)) {
                entry = entry.next;
            }
            if (entry == null) {
                if (storage.length < pos+sbytes.length) {
                    int newStorSize = storage.length;
                    while (newStorSize < pos+sbytes.length) {
                        newStorSize<<=1;
                    }
                    final byte[] tmpStorage = new byte[newStorSize];
                    System.arraycopy(storage, 0, tmpStorage, 0, storage.length);
                    storage = tmpStorage;
                }
                System.arraycopy(sbytes, 0, storage, pos, sbytes.length);
                entry = new Entry(pos, sbytes.length, slots[hc]);
                slots[hc] = entry;
                pos+= sbytes.length == 0 ? 1 : sbytes.length;
            }
            return entry.pos;
        }

        @Override
        public Integer getName(@NonNull final String name) {
            assert name != null;
            final int hc = name.hashCode() & hashMask;
            final byte[] sbytes = decode(name);
            Entry entry = slots[hc];
            while (entry != null && !contentEquals(entry,sbytes)) {
                entry = entry.next;
            }
            return entry == null ? null : entry.pos;
        }

        private boolean contentEquals(
            @NonNull final Entry entry,
            @NonNull final byte[] content) {
            assert entry != null;
            assert content != null;
            if (entry.length != content.length) {
                return false;
            }
            for (int i=0; i< entry.length; i++) {
                if (content[i] != storage[entry.pos+i]) {
                    return false;
                }
            }
            return true;
        }

        private byte[] decode(String str) {
            try {
                return str.getBytes("UTF-8");   //NOI18N
            } catch (UnsupportedEncodingException e) {
                throw new IllegalStateException("No UTF-8 supported");  //Should never happen
            }
        }

        private static class Entry {
            private int pos;
            private int length;
            private Entry next;

            private Entry(
                final int pos,
                final int length,
                @NullAllowed final Entry next) {
                this.pos = pos;
                this.length = length;
                this.next = next;
            }
        }
    }
    //</editor-fold>
}
