/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.plugin.java.server;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import org.eclipse.che.commons.lang.IoUtil;
import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.ide.ext.java.shared.Jar;
import org.eclipse.che.ide.ext.java.shared.JarEntry;
import org.eclipse.che.ide.ext.java.shared.JarEntry.JarEntryType;
import org.eclipse.che.ide.ext.java.shared.OpenDeclarationDescriptor;
import org.eclipse.che.ide.ext.java.shared.dto.ClassContent;
import org.eclipse.che.ide.ext.java.shared.dto.Region;
import org.eclipse.che.ide.ext.java.shared.dto.model.CompilationUnit;
import org.eclipse.che.ide.ext.java.shared.dto.model.Field;
import org.eclipse.che.ide.ext.java.shared.dto.model.Initializer;
import org.eclipse.che.ide.ext.java.shared.dto.model.JavaProject;
import org.eclipse.che.ide.ext.java.shared.dto.model.Member;
import org.eclipse.che.ide.ext.java.shared.dto.model.Method;
import org.eclipse.che.ide.ext.java.shared.dto.model.PackageFragment;
import org.eclipse.che.ide.ext.java.shared.dto.model.PackageFragmentRoot;
import org.eclipse.che.ide.ext.java.shared.dto.model.Type;
import org.eclipse.che.jdt.javadoc.JavaElementLabels;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICodeAssist;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IInitializer;
import org.eclipse.jdt.core.IJarEntryResource;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.internal.core.JarEntryDirectory;
import org.eclipse.jdt.internal.core.JarEntryFile;
import org.eclipse.jdt.internal.core.JarEntryResource;
import org.eclipse.jdt.internal.core.JarPackageFragmentRoot;
import org.eclipse.jdt.internal.core.JavaModel;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.JavaModelStatus;
import org.eclipse.jdt.internal.corext.util.SuperTypeHierarchyCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Manager for java navigation operations.
 * Contains methods that convert jdt Java models to DTO objects.
 *
 * @author Evgen Vidolob
 * @author Valeriy Svydenko
 */
@Singleton
public class JavaNavigation {
    private static final Logger               LOG           = LoggerFactory.getLogger(JavaNavigation.class);
    private static final ArrayList<JarEntry>  NO_ENTRIES    = new ArrayList<>(1);
    private static       Comparator<JarEntry> comparator    = new Comparator<JarEntry>() {
        @Override
        public int compare(JarEntry o1, JarEntry o2) {
            if (o1.getType() == JarEntryType.PACKAGE && o2.getType() != JarEntryType.PACKAGE) {
                return 1;
            }

            if (o2.getType() == JarEntryType.PACKAGE && o1.getType() != JarEntryType.PACKAGE) {
                return 1;
            }

            if (o1.getType() == JarEntryType.CLASS_FILE && o2.getType() != JarEntryType.CLASS_FILE) {
                return 1;
            }

            if (o1.getType() != JarEntryType.CLASS_FILE && o2.getType() == JarEntryType.CLASS_FILE) {
                return 1;
            }

            if (o1.getType() == JarEntryType.FOLDER && o2.getType() != JarEntryType.FOLDER) {
                return 1;
            }

            if (o1.getType() != JarEntryType.FOLDER && o2.getType() == JarEntryType.FOLDER) {
                return 1;
            }

            if (o1.getType() == JarEntryType.FILE && o2.getType() != JarEntryType.FILE) {
                return -1;
            }

            if (o1.getType() != JarEntryType.FILE && o2.getType() == JarEntryType.FILE) {
                return -1;
            }


            if (o1.getType() == o2.getType()) {
                return o1.getName().compareTo(o2.getName());
            }

            return 0;
        }
    };
    private              Gson                 gson          = new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
    private              boolean              fFoldPackages = true;
    private SourcesFromBytecodeGenerator sourcesGenerator;

    @Inject
    public JavaNavigation(SourcesFromBytecodeGenerator sourcesGenerator) {
        this.sourcesGenerator = sourcesGenerator;
    }

    /**
     * Utility method to concatenate two arrays.
     *
     * @param a1
     *         the first array
     * @param a2
     *         the second array
     * @return the concatenated array
     */
    protected static Object[] concatenate(Object[] a1, Object[] a2) {
        int a1Len = a1.length;
        int a2Len = a2.length;
        if (a1Len == 0) return a2;
        if (a2Len == 0) return a1;
        Object[] res = new Object[a1Len + a2Len];
        System.arraycopy(a1, 0, res, 0, a1Len);
        System.arraycopy(a2, 0, res, a1Len, a2Len);
        return res;
    }

    private static IPackageFragment getFolded(IJavaElement[] children, IPackageFragment pack) throws JavaModelException {
        while (isEmpty(pack)) {
            IPackageFragment collapsed = findSinglePackageChild(pack, children);
            if (collapsed == null) {
                return pack;
            }
            pack = collapsed;
        }
        return pack;
    }

    private static boolean isEmpty(IPackageFragment fragment) throws JavaModelException {
        return !fragment.containsJavaResources() && fragment.getNonJavaResources().length == 0;
    }

    private static IPackageFragment findSinglePackageChild(IPackageFragment fragment, IJavaElement[] children) {
        String prefix = fragment.getElementName() + '.';
        int prefixLen = prefix.length();
        IPackageFragment found = null;
        for (int i = 0; i < children.length; i++) {
            IJavaElement element = children[i];
            String name = element.getElementName();
            if (name.startsWith(prefix) && name.length() > prefixLen && name.indexOf('.', prefixLen) == -1) {
                if (found == null) {
                    found = (IPackageFragment)element;
                } else {
                    return null;
                }
            }
        }
        return found;
    }

    public OpenDeclarationDescriptor findDeclaration(IJavaProject project, String fqn, int offset) throws JavaModelException {
        IJavaElement originalElement = null;
        IType type = project.findType(fqn);
        ICodeAssist codeAssist;
        if (type.isBinary()) {
            codeAssist = type.getClassFile();
        } else {
            codeAssist = type.getCompilationUnit();
        }

        IJavaElement[] elements = null;
        if (codeAssist != null) {
            elements = codeAssist.codeSelect(offset, 0);
        }

        if (elements != null && elements.length > 0) {
            originalElement = elements[0];
        }
        IJavaElement element = originalElement;
        while (element != null) {
            if (element instanceof ICompilationUnit) {
                ICompilationUnit unit = ((ICompilationUnit)element).getPrimary();
                return compilationUnitNavigation(unit, originalElement);
            }

            if (element instanceof IClassFile) {
                return classFileNavigation((IClassFile)element, originalElement);
            }
            element = element.getParent();
        }
        return null;
    }

    public List<Jar> getProjectDependecyJars(IJavaProject project) throws JavaModelException {
        List<Jar> jars = new ArrayList<>();
        for (IPackageFragmentRoot fragmentRoot : project.getAllPackageFragmentRoots()) {
            if (fragmentRoot instanceof JarPackageFragmentRoot) {
                Jar jar = DtoFactory.getInstance().createDto(Jar.class);
                jar.setId(fragmentRoot.hashCode());
                jar.setName(fragmentRoot.getElementName());
                jars.add(jar);
            }
        }

        return jars;
    }

    public List<JarEntry> getPackageFragmentRootContent(IJavaProject project, int hash) throws JavaModelException {
        IPackageFragmentRoot packageFragmentRoot = getPackageFragmentRoot(project, hash);

        if (packageFragmentRoot == null) {
            return NO_ENTRIES;
        }

        Object[] rootContent = getPackageFragmentRootContent(packageFragmentRoot);

        return convertToJarEntry(rootContent, packageFragmentRoot);
    }

    /**
     * Get the compilation unit representation of the java file.
     *
     * @param javaProject
     *         path to the project which is contained class file
     * @param fqn
     *         fully qualified name of the class file
     * @param isShowingInheritedMembers
     *         <code>true</code> iff inherited members are shown
     * @return instance of {@link CompilationUnit}
     * @throws JavaModelException
     *         when JavaModel has a failure
     */
    public CompilationUnit getCompilationUnitByPath(IJavaProject javaProject,
                                                    String fqn,
                                                    boolean isShowingInheritedMembers) throws JavaModelException {
        IType type = javaProject.findType(fqn);
        CompilationUnit compilationUnit = DtoFactory.newDto(CompilationUnit.class);
        ITypeRoot unit;
        if (type.isBinary()) {
            unit = type.getClassFile();
            compilationUnit.setPath(((IClassFile)unit).getType().getFullyQualifiedName());
        } else {
            unit = type.getCompilationUnit();
            compilationUnit.setProjectPath(unit.getJavaProject().getPath().toOSString());
            compilationUnit.setPath(unit.getResource().getFullPath().toOSString());
        }

        compilationUnit.setElementName(unit.getElementName());
        compilationUnit.setHandleIdentifier(unit.getHandleIdentifier());
        compilationUnit.setLabel(org.eclipse.jdt.ui.JavaElementLabels.getElementLabel(unit,
                                                                                      org.eclipse.jdt.ui.JavaElementLabels.ALL_DEFAULT));
        List<Type> types = new ArrayList<>(1);
        Type dtoType = convertToDTOType(type);
        dtoType.setPrimary(true);
        types.add(dtoType);
        compilationUnit.setTypes(types);

        if (isShowingInheritedMembers) {
            compilationUnit.setSuperTypes(calculateSuperTypes(type));
        }

        return compilationUnit;
    }

    private List<Type> calculateSuperTypes(IType type) throws JavaModelException {
        List<Type> superTypes = new ArrayList<>();
        ITypeHierarchy superTypeHierarchy = SuperTypeHierarchyCache.getTypeHierarchy(type);
        if (superTypeHierarchy != null) {
            IType[] superITypes = superTypeHierarchy.getAllSupertypes(type);
            for (IType iType : superITypes) {
                superTypes.add(convertToDTOType(iType));
            }
        }
        return superTypes;
    }

    private Type convertToDTOType(IType iType) throws JavaModelException {
        List<Type> types = new ArrayList<>();
        List<Method> methods = new ArrayList<>();
        List<Field> fields = new ArrayList<>();
        List<Initializer> initializers = new ArrayList<>();

        Type type = DtoFactory.newDto(Type.class);

        setRootPath(iType, type);

        type.setElementName(iType.getElementName());
        type.setLabel(
                org.eclipse.jdt.ui.JavaElementLabels.getElementLabel(iType, org.eclipse.jdt.ui.JavaElementLabels.ALL_DEFAULT));
        type.setHandleIdentifier(iType.getHandleIdentifier());
        type.setFlags(iType.getFlags());
        type.setFileRegion(convertToDTORegion(iType.getNameRange()));

        if (!iType.hasChildren()) {
            type.setTypes(types);
            return type;
        }

        IJavaElement[] children = iType.getChildren();
        for (IJavaElement child : children) {
            switch (child.getElementType()) {
                case 7: //type
                    types.add(convertToDTOType((IType)child));
                    break;
                case 8: //field
                    fields.add(convertToDTOField((IField)child));
                    break;
                case 9: //method
                    methods.add(convertToDTOMethod((IMethod)child));
                    break;
                case 10: //initializer
                    initializers.add(convertToDTOInitializer((IInitializer)child));
                    break;
                default:
                    break;
            }
        }

        type.setFields(fields);
        type.setMethods(methods);
        type.setInitializers(initializers);
        type.setTypes(types);

        return type;
    }

    private Field convertToDTOField(IField iField) throws JavaModelException {
        Field field = DtoFactory.newDto(Field.class);

        setRootPath(iField, field);

        field.setFileRegion(convertToDTORegion(iField.getNameRange()));
        field.setElementName(iField.getElementName());
        field.setHandleIdentifier(iField.getHandleIdentifier());
        field.setFlags(iField.getFlags());
        field.setLabel(org.eclipse.jdt.ui.JavaElementLabels.getElementLabel(iField, org.eclipse.jdt.ui.JavaElementLabels.ALL_DEFAULT));

        return field;
    }

    private Method convertToDTOMethod(IMethod iMethod) throws JavaModelException {
        Method method = DtoFactory.newDto(Method.class);

        setRootPath(iMethod, method);

        method.setFileRegion(convertToDTORegion(iMethod.getNameRange()));
        method.setElementName(iMethod.getElementName());
        method.setReturnType(Signature.toString(iMethod.getReturnType()));
        method.setHandleIdentifier(iMethod.getHandleIdentifier());
        method.setFlags(iMethod.getFlags());
        method.setLabel(org.eclipse.jdt.ui.JavaElementLabels.getElementLabel(iMethod, org.eclipse.jdt.ui.JavaElementLabels.ALL_DEFAULT));

        return method;
    }

    private void setRootPath(IMember iMember, Member member) {
        if (iMember.isBinary()) {
            member.setBinary(true);
            member.setRootPath(iMember.getClassFile().getType().getFullyQualifiedName());
            member.setLibId(iMember.getClassFile().getAncestor(IPackageFragmentRoot.PACKAGE_FRAGMENT_ROOT).hashCode());
        } else {
            member.setBinary(false);
            member.setRootPath(iMember.getCompilationUnit().getPath().toOSString());
        }
    }

    private Initializer convertToDTOInitializer(IInitializer iInitializer) throws JavaModelException {
        Initializer initializer = DtoFactory.newDto(Initializer.class);

        initializer.setFileRegion(convertToDTORegion(iInitializer.getSourceRange()));
        initializer.setElementName(iInitializer.getElementName());
        initializer.setHandleIdentifier(iInitializer.getHandleIdentifier());
        initializer.setFlags(iInitializer.getFlags());
        initializer.setLabel(org.eclipse.jdt.ui.JavaElementLabels.getElementLabel(iInitializer,
                                                                                  org.eclipse.jdt.ui.JavaElementLabels.ALL_DEFAULT));

        return initializer;
    }

    private Region convertToDTORegion(ISourceRange iSourceRange) {
        Region region = DtoFactory.newDto(Region.class);
        return iSourceRange == null ? region : region.withLength(iSourceRange.getLength()).withOffset(iSourceRange.getOffset());
    }

    private IPackageFragmentRoot getPackageFragmentRoot(IJavaProject project, int hash) throws JavaModelException {
        IPackageFragmentRoot[] roots = project.getAllPackageFragmentRoots();
        IPackageFragmentRoot packageFragmentRoot = null;
        for (IPackageFragmentRoot root : roots) {
            if (root.hashCode() == hash) {
                packageFragmentRoot = root;
                break;
            }
        }
        return packageFragmentRoot;
    }

    private List<JarEntry> convertToJarEntry(Object[] rootContent, IPackageFragmentRoot root) throws JavaModelException {
        List<JarEntry> result = new ArrayList<>();
        for (Object o : rootContent) {
            if (o instanceof IPackageFragment) {
                JarEntry entry = DtoFactory.getInstance().createDto(JarEntry.class);
                IPackageFragment packageFragment = (IPackageFragment)o;
                entry.setName(getSpecificText((IJavaElement)o));
                entry.setPath(packageFragment.getElementName());
                entry.setType(JarEntryType.PACKAGE);
                result.add(entry);
            }

            if (o instanceof IClassFile) {
                JarEntry entry = getJarClass((IClassFile)o);
                result.add(entry);
            }

            if (o instanceof JarEntryResource) {
                result.add(getJarEntryResource((JarEntryResource)o));
            }
        }
        Collections.sort(result, comparator);
        return result;
    }

    private JarEntry getJarClass(IClassFile classFile) {
        JarEntry entry = DtoFactory.getInstance().createDto(JarEntry.class);
        entry.setType(JarEntryType.CLASS_FILE);
        entry.setName(classFile.getElementName());
        entry.setPath(classFile.getType().getFullyQualifiedName());
        return entry;
    }

    private String getSpecificText(IJavaElement element) {
        if (element instanceof IPackageFragment) {
            IPackageFragment fragment = (IPackageFragment)element;
            Object parent = getHierarchicalPackageParent(fragment);
            if (parent instanceof IPackageFragment) {
                return getNameDelta((IPackageFragment)parent, fragment);
            }
        }

        return JavaElementLabels.getElementLabel(element, 0);
    }

    private String getNameDelta(IPackageFragment parent, IPackageFragment fragment) {
        String prefix = parent.getElementName() + '.';
        String fullName = fragment.getElementName();
        if (fullName.startsWith(prefix)) {
            return fullName.substring(prefix.length());
        }
        return fullName;
    }

    public Object getHierarchicalPackageParent(IPackageFragment child) {
        String name = child.getElementName();
        IPackageFragmentRoot parent = (IPackageFragmentRoot)child.getParent();
        int index = name.lastIndexOf('.');
        if (index != -1) {
            String realParentName = name.substring(0, index);
            IPackageFragment element = parent.getPackageFragment(realParentName);
            if (element.exists()) {
                try {
                    if (fFoldPackages && isEmpty(element) && findSinglePackageChild(element, parent.getChildren()) != null) {
                        return getHierarchicalPackageParent(element);
                    }
                } catch (JavaModelException e) {
                    // ignore
                }
                return element;
            } /*else { // bug 65240
                IResource resource= element.getResource();
                if (resource != null) {
                    return resource;
                }
            }*/
        }
//        if (parent.getResource() instanceof IProject) {
//            return parent.getJavaProject();
//        }
        return parent;
    }

    private JarEntry getJarEntryResource(JarEntryResource resource) {
        JarEntry entry = DtoFactory.getInstance().createDto(JarEntry.class);
        if (resource instanceof JarEntryDirectory) {
            entry.setType(JarEntryType.FOLDER);
        }
        if (resource instanceof JarEntryFile) {
            entry.setType(JarEntryType.FILE);
        }
        entry.setName(resource.getName());
        entry.setPath(resource.getFullPath().toOSString());
        return entry;
    }

    protected Object[] getPackageFragmentRootContent(IPackageFragmentRoot root) throws JavaModelException {

        // hierarchical package mode
        ArrayList<Object> result = new ArrayList<>();
        getHierarchicalPackageChildren(root, null, result);
        Object[] nonJavaResources = root.getNonJavaResources();
        for (int i = 0; i < nonJavaResources.length; i++) {
            result.add(nonJavaResources[i]);
        }
        return result.toArray();
    }

    /* (non-Javadoc)
 * @see org.eclipse.jdt.ui.StandardJavaElementContentProvider#getPackageContent(org.eclipse.jdt.core.IPackageFragment)
 */
    protected Object[] getPackageContent(IPackageFragment fragment) throws JavaModelException {

        // hierarchical package mode
        ArrayList<Object> result = new ArrayList<Object>();

        getHierarchicalPackageChildren((IPackageFragmentRoot)fragment.getParent(), fragment, result);
        IClassFile[] classFiles = fragment.getClassFiles();
        List<IClassFile> filtered = new ArrayList<>();
        //filter inner classes
        for (IClassFile classFile : classFiles) {
            if (!classFile.getElementName().contains("$")) {
                filtered.add(classFile);
            }
        }
        Object[] nonPackages = concatenate(filtered.toArray(), fragment.getNonJavaResources());
        if (result.isEmpty())
            return nonPackages;
        Collections.addAll(result, nonPackages);
        return result.toArray();
    }

    /**
     * Returns the hierarchical packages inside a given fragment or root.
     *
     * @param parent
     *         the parent package fragment root
     * @param fragment
     *         the package to get the children for or 'null' to get the children of the root
     * @param result
     *         Collection where the resulting elements are added
     * @throws JavaModelException
     *         if fetching the children fails
     */
    private void getHierarchicalPackageChildren(IPackageFragmentRoot parent, IPackageFragment fragment, Collection<Object> result)
            throws JavaModelException {
        IJavaElement[] children = parent.getChildren();
        String prefix = fragment != null ? fragment.getElementName() + '.' : ""; //$NON-NLS-1$
        int prefixLen = prefix.length();
        for (int i = 0; i < children.length; i++) {
            IPackageFragment curr = (IPackageFragment)children[i];
            String name = curr.getElementName();
            if (name.startsWith(prefix) && name.length() > prefixLen && name.indexOf('.', prefixLen) == -1) {
                if (fFoldPackages) {
                    curr = getFolded(children, curr);
                }
                result.add(curr);
            } else if (fragment == null && curr.isDefaultPackage()) {
                IJavaElement[] currChildren = curr.getChildren();
                if (currChildren != null && currChildren.length >= 1) {
                    result.add(curr);
                }
            }
        }
    }

    private OpenDeclarationDescriptor classFileNavigation(IClassFile classFile, IJavaElement element) throws JavaModelException {
        OpenDeclarationDescriptor dto = DtoFactory.getInstance().createDto(OpenDeclarationDescriptor.class);
        dto.setPath(classFile.getType().getFullyQualifiedName());
        dto.setLibId(classFile.getAncestor(IPackageFragmentRoot.PACKAGE_FRAGMENT_ROOT).hashCode());
        dto.setBinary(true);
        if (classFile.getSourceRange() != null) {
            if (element instanceof ISourceReference) {
                ISourceRange nameRange = ((ISourceReference)element).getNameRange();
                dto.setOffset(nameRange.getOffset());
                dto.setLength(nameRange.getLength());
            }
        }
        return dto;
    }

    private OpenDeclarationDescriptor compilationUnitNavigation(ICompilationUnit unit, IJavaElement element)
            throws JavaModelException {
        OpenDeclarationDescriptor dto = DtoFactory.getInstance().createDto(OpenDeclarationDescriptor.class);
        String absolutePath = unit.getPath().toOSString();
        dto.setPath(absolutePath);
        dto.setBinary(false);
        if (element instanceof ISourceReference) {
            ISourceRange nameRange = ((ISourceReference)element).getNameRange();
            dto.setOffset(nameRange.getOffset());
            dto.setLength(nameRange.getLength());
        }

        return dto;
    }

    private Object[] findJarDirectoryChildren(JarEntryDirectory directory, String path) {
        String directoryPath = directory.getFullPath().toOSString();
        if (directoryPath.equals(path)) {
            return directory.getChildren();
        }
        if (path.startsWith(directoryPath)) {
            for (IJarEntryResource resource : directory.getChildren()) {
                String childrenPath = resource.getFullPath().toOSString();
                if (childrenPath.equals(path)) {
                    return resource.getChildren();
                }
                if (path.startsWith(childrenPath) && resource instanceof JarEntryDirectory) {
                    findJarDirectoryChildren((JarEntryDirectory)resource, path);
                }
            }
        }
        return null;
    }

    public List<JarEntry> getChildren(IJavaProject project, int rootId, String path) throws JavaModelException {
        IPackageFragmentRoot root = getPackageFragmentRoot(project, rootId);
        if (root == null) {
            return NO_ENTRIES;
        }

        if (path.startsWith("/")) {
            // jar file and folders
            Object[] resources = root.getNonJavaResources();
            for (Object resource : resources) {
                if (resource instanceof JarEntryDirectory) {
                    JarEntryDirectory directory = (JarEntryDirectory)resource;
                    Object[] children = findJarDirectoryChildren(directory, path);
                    if (children != null) {
                        return convertToJarEntry(children, root);
                    }
                }
            }

        } else {
            // packages and class files
            IPackageFragment fragment = root.getPackageFragment(path);
            if (fragment == null) {
                return NO_ENTRIES;
            }
            return convertToJarEntry(getPackageContent(fragment), root);
        }
        return NO_ENTRIES;
    }

    public ClassContent getContent(IJavaProject project, int rootId, String path) throws CoreException {
        IPackageFragmentRoot root = getPackageFragmentRoot(project, rootId);
        if (root == null) {
            return null;
        }

        if (path.startsWith("/")) {
            //non java file
            if (root instanceof JarPackageFragmentRoot) {
                JarPackageFragmentRoot jarPackageFragmentRoot = (JarPackageFragmentRoot)root;
                ZipFile jar = null;
                try {
                    jar = jarPackageFragmentRoot.getJar();
                    ZipEntry entry = jar.getEntry(path.substring(1));
                    if (entry != null) {
                        try (InputStream stream = jar.getInputStream(entry)) {
                            return createContent(IoUtil.readStream(stream), false);
                        } catch (IOException e) {
                            LOG.error("Can't read file content: " + entry.getName(), e);
                        }
                    }
                } finally {
                    if (jar != null) {
                        JavaModelManager.getJavaModelManager().closeZipFile(jar);
                    }
                }
            }
            Object[] resources = root.getNonJavaResources();

            for (Object resource : resources) {
                if (resource instanceof JarEntryFile) {
                    JarEntryFile file = (JarEntryFile)resource;
                    if (file.getFullPath().toOSString().equals(path)) {
                        return readFileContent(file);
                    }
                }
                if (resource instanceof JarEntryDirectory) {
                    JarEntryDirectory directory = (JarEntryDirectory)resource;
                    JarEntryFile file = findJarFile(directory, path);
                    if (file != null) {
                        return readFileContent(file);
                    }
                }
            }
        } else {
            return getContent(project, path);
        }
        return null;
    }

    public ClassContent getContent(IJavaProject project, String path) throws JavaModelException {
        //java class or file
        IType type = project.findType(path);
        if (type != null) {
            if (type.isBinary()) {
                IClassFile classFile = type.getClassFile();
                if (classFile.getSourceRange() != null) {
                    return createContent(classFile.getSource(), false);
                } else {

                    return createContent(sourcesGenerator.generateSource(classFile.getType()), true);
                }
            } else {
                return createContent(type.getCompilationUnit().getSource(), false);
            }
        }
        throw new JavaModelException(new JavaModelStatus(0, "Can't find type: " + path));
    }

    private ClassContent readFileContent(JarEntryFile file) {
        try (InputStream stream = (file.getContents())) {
            return createContent(IoUtil.readStream(stream), false);
        } catch (IOException | CoreException e) {
            LOG.error("Can't read file content: " + file.getFullPath(), e);
        }
        return null;
    }

    private ClassContent createContent(String content, boolean generated) {
        ClassContent classContent = DtoFactory.newDto(ClassContent.class);
        classContent.setContent(content);
        classContent.setGenerated(generated);
        return classContent;
    }

    private JarEntryFile findJarFile(JarEntryDirectory directory, String path) {
        for (IJarEntryResource children : directory.getChildren()) {
            if (children.isFile() && children.getFullPath().toOSString().equals(path)) {
                return (JarEntryFile)children;
            }
            if (!children.isFile()) {
                JarEntryFile file = findJarFile((JarEntryDirectory)children, path);
                if (file != null) {
                    return file;
                }
            }
        }
        return null;
    }

    public JarEntry getEntry(IJavaProject project, int rootId, String path) throws CoreException {
        IPackageFragmentRoot root = getPackageFragmentRoot(project, rootId);
        if (root == null) {
            return null;
        }
        if (path.startsWith("/")) {

            JarPackageFragmentRoot jarPackageFragmentRoot = (JarPackageFragmentRoot)root;
            ZipFile jar = null;
            try {
                jar = jarPackageFragmentRoot.getJar();
                ZipEntry entry = jar.getEntry(path.substring(1));
                if (entry != null) {
                    JarEntry result = DtoFactory.getInstance().createDto(JarEntry.class);
                    result.setType(JarEntryType.FILE);
                    result.setPath(path);
                    result.setName(entry.getName().substring(entry.getName().lastIndexOf("/") + 1));
                    return result;
                }
            } finally {
                if (jar != null) {
                    JavaModelManager.getJavaModelManager().closeZipFile(jar);
                }
            }

            Object[] resources = root.getNonJavaResources();

            for (Object resource : resources) {
                if (resource instanceof JarEntryFile) {
                    JarEntryFile file = (JarEntryFile)resource;
                    if (file.getFullPath().toOSString().equals(path)) {
                        return getJarEntryResource(file);
                    }
                }
                if (resource instanceof JarEntryDirectory) {
                    JarEntryDirectory directory = (JarEntryDirectory)resource;
                    JarEntryFile file = findJarFile(directory, path);
                    if (file != null) {
                        return getJarEntryResource(file);
                    }
                }
            }

        } else {
            //java class or file
            IType type = project.findType(path);
            if (type != null && type.isBinary()) {
                IClassFile classFile = type.getClassFile();
                return getJarClass(classFile);
            }
        }

        return null;
    }

    public List<JavaProject> getAllProjectsAndPackages(boolean includePackages) throws JavaModelException {
        JavaModel javaModel = JavaModelManager.getJavaModelManager().getJavaModel();
        IJavaProject[] javaProjects = javaModel.getJavaProjects();
        List<JavaProject> result = new ArrayList<>();
        for (IJavaProject javaProject : javaProjects) {
            if (javaProject.exists()) {
                JavaProject project = DtoFactory.newDto(JavaProject.class);
                project.setName(javaProject.getElementName());
                project.setPath(javaProject.getPath().toOSString());
                project.setPackageFragmentRoots(toPackageRoots(javaProject, includePackages));
                result.add(project);
            }
        }
        return result;
    }

    private List<PackageFragmentRoot> toPackageRoots(IJavaProject javaProject, boolean includePackages) throws JavaModelException {
        IPackageFragmentRoot[] packageFragmentRoots = javaProject.getAllPackageFragmentRoots();
        List<PackageFragmentRoot> result = new ArrayList<>();
        for (IPackageFragmentRoot packageFragmentRoot : packageFragmentRoots) {
            if (packageFragmentRoot.getKind() == IPackageFragmentRoot.K_SOURCE &&
                javaProject.getPath().isPrefixOf(packageFragmentRoot.getPath())) {
                PackageFragmentRoot root = DtoFactory.newDto(PackageFragmentRoot.class);
                root.setPath(packageFragmentRoot.getPath().toOSString());
                root.setProjectPath(packageFragmentRoot.getJavaProject().getPath().toOSString());
                if (includePackages) {
                    root.setPackageFragments(toPackageFragments(packageFragmentRoot));
                }
                result.add(root);
            }
        }
        return result;
    }

    private List<PackageFragment> toPackageFragments(IPackageFragmentRoot packageFragmentRoot) throws JavaModelException {
        IJavaElement[] children = packageFragmentRoot.getChildren();
        if (children == null) {
            return null;
        }
        List<PackageFragment> result = new ArrayList<>();
        for (IJavaElement child : children) {
            if (child instanceof IPackageFragment) {
                IPackageFragment packageFragment = (IPackageFragment)child;
                PackageFragment fragment = DtoFactory.newDto(PackageFragment.class);
                fragment.setElementName(packageFragment.getElementName());
                fragment.setPath(packageFragment.getPath().toOSString());
                fragment.setProjectPath(packageFragment.getJavaProject().getPath().toOSString());
                result.add(fragment);
            }
        }
        return result;
    }
}
