org.structr.web.entity.File.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.web.entity.File.java

Source

/**
 * Copyright (C) 2010-2018 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.web.entity;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.activation.DataSource;
import javax.xml.stream.XMLStreamException;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.structr.api.config.Settings;
import org.structr.cmis.CMISInfo;
import org.structr.cmis.info.CMISDocumentInfo;
import org.structr.common.ConstantBooleanTrue;
import org.structr.common.Permission;
import org.structr.common.PropertyView;
import org.structr.common.SecurityContext;
import org.structr.common.error.ErrorBuffer;
import org.structr.common.error.FrameworkException;
import org.structr.common.error.UnlicensedScriptException;
import org.structr.common.fulltext.FulltextIndexer;
import org.structr.common.fulltext.Indexable;
import org.structr.core.GraphObject;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.Favoritable;
import org.structr.core.entity.Principal;
import org.structr.core.function.Functions;
import org.structr.core.graph.ModificationEvent;
import org.structr.core.graph.ModificationQueue;
import org.structr.core.graph.Tx;
import org.structr.core.property.PropertyKey;
import org.structr.core.scheduler.JobQueueManager;
import org.structr.core.script.Scripting;
import org.structr.files.cmis.config.StructrFileActions;
import org.structr.rest.common.XMLStructureAnalyzer;
import org.structr.schema.SchemaService;
import org.structr.schema.action.ActionContext;
import org.structr.schema.action.Function;
import org.structr.schema.action.JavaScriptSource;
import org.structr.schema.json.JsonMethod;
import org.structr.schema.json.JsonObjectType;
import org.structr.schema.json.JsonSchema;
import org.structr.web.common.ClosingFileOutputStream;
import org.structr.web.common.FileHelper;
import org.structr.web.common.RenderContext;
import org.structr.web.importer.CSVFileImportJob;
import org.structr.web.importer.XMLFileImportJob;
import org.structr.web.property.FileDataProperty;

/**
 *
 *
 */
public interface File extends AbstractFile, Indexable, Linkable, JavaScriptSource, CMISInfo, CMISDocumentInfo,
        Favoritable, DataSource {

    static class Impl {
        static {

            final JsonSchema schema = SchemaService.getDynamicSchema();
            final JsonObjectType type = schema.addType("File");

            type.setImplements(URI.create("https://structr.org/v1.1/definitions/File"));
            type.setImplements(URI.create("#/definitions/Indexable"));
            type.setImplements(URI.create("#/definitions/Linkable"));
            type.setImplements(URI.create("#/definitions/JavaScriptSource"));
            type.setImplements(URI.create("#/definitions/Favoritable"));
            type.setExtends(URI.create("#/definitions/AbstractFile"));
            type.setCategory("core");

            type.addStringProperty("url", PropertyView.Public, PropertyView.Ui);

            type.addBooleanProperty("isFile", PropertyView.Public, PropertyView.Ui).setReadOnly(true)
                    .addTransformer(ConstantBooleanTrue.class.getName());
            type.addBooleanProperty("isTemplate", PropertyView.Public, PropertyView.Ui);
            type.addLongProperty("size", PropertyView.Public, PropertyView.Ui).setIndexed(true);
            type.addLongProperty("fileModificationDate", PropertyView.Public);

            type.addIntegerProperty("cacheForSeconds", PropertyView.Ui);
            type.addBooleanProperty("dontCache", PropertyView.Public, PropertyView.Ui).setDefaultValue("false");
            type.addIntegerProperty("version", PropertyView.Ui).setIndexed(true);
            type.addLongProperty("checksum", PropertyView.Ui).setIndexed(true);
            type.addStringProperty("md5", PropertyView.Ui);
            type.addLongProperty("crc32").setIndexed(true);
            type.addStringProperty("sha1");
            type.addStringProperty("sha512");
            type.addIntegerProperty("position").setIndexed(true);

            type.addPropertyGetter("minificationTargets", List.class);
            type.addPropertyGetter("cacheForSeconds", Integer.class);
            type.addPropertyGetter("checksum", Long.class);
            type.addPropertyGetter("md5", String.class);

            type.addPropertyGetter("version", Integer.class);
            type.addPropertyGetter("contentType", String.class);
            type.addPropertyGetter("extractedContent", String.class);
            type.addPropertyGetter("basicAuthRealm", String.class);
            type.addPropertyGetter("size", Long.class);

            type.addCustomProperty("base64Data", FileDataProperty.class.getName()).setTypeHint("String");

            type.overrideMethod("onCreation", true, File.class.getName() + ".onCreation(this, arg0, arg1);");
            type.overrideMethod("onModification", true,
                    File.class.getName() + ".onModification(this, arg0, arg1, arg2);");
            type.overrideMethod("onNodeDeletion", true, File.class.getName() + ".onNodeDeletion(this);");
            type.overrideMethod("afterCreation", true, File.class.getName() + ".afterCreation(this, arg0);");

            type.overrideMethod("isTemplate", false, "return getProperty(isTemplateProperty);");
            type.overrideMethod("setVersion", false, "setProperty(versionProperty, arg0);")
                    .addException(FrameworkException.class.getName());

            type.overrideMethod("increaseVersion", false, File.class.getName() + ".increaseVersion(this);");
            type.overrideMethod("notifyUploadCompletion", false,
                    File.class.getName() + ".notifyUploadCompletion(this);");
            type.overrideMethod("triggerMinificationIfNeeded", false,
                    File.class.getName() + ".triggerMinificationIfNeeded(this, arg0);");

            type.overrideMethod("getInputStream", false,
                    "return " + File.class.getName() + ".getInputStream(this);");
            type.overrideMethod("getSearchContext", false,
                    "return " + File.class.getName() + ".getSearchContext(this, arg0, arg1);");
            type.overrideMethod("getJavascriptLibraryCode", false,
                    "return " + File.class.getName() + ".getJavascriptLibraryCode(this);");
            type.overrideMethod("getEnableBasicAuth", false, "return getProperty(enableBasicAuthProperty);");

            // Favoritable
            type.overrideMethod("getContext", false, "return getPath();");
            type.overrideMethod("getFavoriteContentType", false, "return getContentType();");
            type.overrideMethod("setFavoriteContent", false,
                    File.class.getName() + ".setFavoriteContent(this, arg0);");
            type.overrideMethod("getFavoriteContent", false,
                    "return " + File.class.getName() + ".getFavoriteContent(this);");
            type.overrideMethod("getCurrentWorkingDir", false,
                    "return " + File.class.getName() + ".getCurrentWorkingDir(this);");

            // CMIS support
            type.overrideMethod("getCMISInfo", false, "return this;");
            type.overrideMethod("getBaseTypeId", false, "return " + BaseTypeId.class.getName() + ".CMIS_DOCUMENT;");
            type.overrideMethod("getFolderInfo", false, "return null;");
            type.overrideMethod("getDocumentInfo", false, "return this;");
            type.overrideMethod("getItemInfo", false, "return null;");
            type.overrideMethod("getRelationshipInfo", false, "return null;");
            type.overrideMethod("getPolicyInfo", false, "return null;");
            type.overrideMethod("getSecondaryInfo", false, "return null;");
            type.overrideMethod("getChangeToken", false, "return null;");
            type.overrideMethod("getParentId", false, "return getProperty(parentIdProperty);");
            type.overrideMethod("getAllowableActions", false,
                    "return new " + StructrFileActions.class.getName() + "(isImmutable());");
            type.overrideMethod("isImmutable", false, "return " + File.class.getName() + ".isImmutable(this);");

            // overridden methods
            final JsonMethod getOutputStream1 = type.addMethod("getOutputStream");
            getOutputStream1.setSource("return " + File.class.getName()
                    + ".getOutputStream(this, notifyIndexerAfterClosing, append);");
            getOutputStream1.addParameter("notifyIndexerAfterClosing", "boolean");
            getOutputStream1.addParameter("append", "boolean");
            getOutputStream1.setReturnType(FileOutputStream.class.getName());

            final JsonMethod getOutputStream2 = type.addMethod("getOutputStream");
            getOutputStream2.setSource("return " + File.class.getName() + ".getOutputStream(this, true, false);");
            getOutputStream2.setReturnType(FileOutputStream.class.getName());

            type.addMethod("getFileOnDisk").setReturnType(java.io.File.class.getName())
                    .setSource("return " + File.class.getName() + ".getFileOnDisk(this);");

            type.addMethod("getFileOnDisk").setReturnType(java.io.File.class.getName())
                    .addParameter("doCreate", "boolean")
                    .setSource("return " + File.class.getName() + ".getFileOnDisk(this, doCreate);");

            type.addMethod("doCSVImport")
                    .addParameter("parameters", "java.util.Map<java.lang.String, java.lang.Object>")
                    .setSource(File.class.getName() + ".doCSVImport(this, parameters);")
                    .addException(FrameworkException.class.getName()).setDoExport(true);

            type.addMethod("doXMLImport")
                    .addParameter("parameters", "java.util.Map<java.lang.String, java.lang.Object>")
                    .setSource(File.class.getName() + ".doXMLImport(this, parameters);")
                    .addException(FrameworkException.class.getName()).setDoExport(true);

            type.addMethod("getFirstLines")
                    .addParameter("parameters", "java.util.Map<java.lang.String, java.lang.Object>")
                    .setReturnType("java.util.Map<java.lang.String, java.lang.Object>")
                    .setSource("return " + File.class.getName() + ".getFirstLines(this, parameters);")
                    .setDoExport(true);

            type.addMethod("getCSVHeaders")
                    .addParameter("parameters", "java.util.Map<java.lang.String, java.lang.Object>")
                    .setReturnType("java.util.Map<java.lang.String, java.lang.Object>")
                    .setSource("return " + File.class.getName() + ".getCSVHeaders(this, parameters);")
                    .addException(FrameworkException.class.getName()).setDoExport(true);

            type.addMethod("getXMLStructure").setReturnType("java.lang.String")
                    .setSource("return " + File.class.getName() + ".getXMLStructure(this);")
                    .addException(FrameworkException.class.getName()).setDoExport(true);

            // view configuration
            type.addViewProperty(PropertyView.Public, "includeInFrontendExport");
            type.addViewProperty(PropertyView.Public, "owner");

            type.addViewProperty(PropertyView.Ui, "hasParent");
            type.addViewProperty(PropertyView.Ui, "path");

            /* TODO:
               public static final Property<List<User>> favoriteOfUsers                     = new StartNodes<>("favoriteOfUsers", UserFavoriteFile.class);
            */

        }
    }

    List<AbstractMinifiedFile> getMinificationTargets();

    String getXMLStructure() throws FrameworkException;

    void doCSVImport(final Map<String, Object> parameters) throws FrameworkException;

    void doXMLImport(final Map<String, Object> parameters) throws FrameworkException;

    Map<String, Object> getCSVHeaders(final Map<String, Object> parameters) throws FrameworkException;

    Map<String, Object> getFirstLines(final Map<String, Object> parameters);

    FileOutputStream getOutputStream(final boolean notifyIndexerAfterClosing, final boolean append);

    java.io.File getFileOnDisk(final boolean doCreate);

    java.io.File getFileOnDisk();

    boolean isTemplate();

    void notifyUploadCompletion();

    void increaseVersion() throws FrameworkException;

    void triggerMinificationIfNeeded(final ModificationQueue modificationQueue) throws FrameworkException;

    void setVersion(final int version) throws FrameworkException;

    Integer getVersion();

    Integer getCacheForSeconds();

    Long getChecksum();

    String getMd5();

    Folder getCurrentWorkingDir();

    static void onCreation(final File thisFile, final SecurityContext securityContext,
            final ErrorBuffer errorBuffer) throws FrameworkException {

        if (Settings.FilesystemEnabled.getValue() && !thisFile.getHasParent()) {

            final Folder workingOrHomeDir = thisFile.getCurrentWorkingDir();
            if (workingOrHomeDir != null && thisFile.getParent() == null) {

                thisFile.setParent(workingOrHomeDir);
            }
        }
    }

    static void onModification(final File thisFile, final SecurityContext securityContext,
            final ErrorBuffer errorBuffer, final ModificationQueue modificationQueue) throws FrameworkException {

        synchronized (thisFile) {

            // save current security context
            final SecurityContext previousSecurityContext = securityContext;

            // replace with SU context
            thisFile.setSecurityContext(SecurityContext.getSuperUserInstance());

            // update metadata and parent as superuser
            FileHelper.updateMetadata(thisFile, false);

            // restore previous security context
            thisFile.setSecurityContext(previousSecurityContext);
        }

        thisFile.triggerMinificationIfNeeded(modificationQueue);
    }

    static void onNodeDeletion(final File thisFile) {

        // only delete mounted files
        if (!thisFile.isExternal()) {

            java.io.File toDelete = thisFile.getFileOnDisk(false);

            try {

                if (toDelete.exists() && toDelete.isFile()) {

                    toDelete.delete();
                }

            } catch (Throwable t) {
                logger.debug("Exception while trying to delete file {}: {}", toDelete.getPath(), t.getMessage());
            }
        }
    }

    static void afterCreation(final File thisFile, final SecurityContext securityContext)
            throws FrameworkException {

        try {

            FileHelper.updateMetadata(thisFile);
            thisFile.setVersion(0);

        } catch (FrameworkException ex) {

            logger.error("Could not update metadata of {}: {}", thisFile.getPath(), ex.getMessage());
        }

    }

    static GraphObject getSearchContext(final File thisFile, final String searchTerm, final int contextLength) {

        final String text = thisFile.getExtractedContent();
        if (text != null) {

            final FulltextIndexer indexer = StructrApp.getInstance(thisFile.getSecurityContext())
                    .getFulltextIndexer();
            return indexer.getContextObject(searchTerm, text, contextLength);
        }

        return null;
    }

    static void notifyUploadCompletion(final File thisFile) {

        try {

            try (final Tx tx = StructrApp.getInstance().tx()) {

                synchronized (tx) {

                    FileHelper.updateMetadata(thisFile, true);

                    tx.success();
                }
            }

            final FulltextIndexer indexer = StructrApp.getInstance(thisFile.getSecurityContext())
                    .getFulltextIndexer();
            indexer.addToFulltextIndex(thisFile);

        } catch (FrameworkException fex) {

            logger.warn("Unable to index {}: {}", thisFile, fex.getMessage());
        }
    }

    static String getFormattedSize(final File thisFile) {
        return FileUtils.byteCountToDisplaySize(thisFile.getSize());
    }

    static void increaseVersion(final File thisFile) throws FrameworkException {

        final Integer _version = thisFile.getVersion();

        thisFile.unlockSystemPropertiesOnce();
        if (_version == null) {

            thisFile.setVersion(1);

        } else {

            thisFile.setVersion(_version + 1);
        }
    }

    static void triggerMinificationIfNeeded(final File thisFile, final ModificationQueue modificationQueue)
            throws FrameworkException {

        final List<AbstractMinifiedFile> targets = thisFile.getMinificationTargets();
        final PropertyKey<Integer> versionKey = StructrApp.key(File.class, "version");

        if (!targets.isEmpty()) {

            // only run minification if the file version changed
            boolean versionChanged = false;
            for (ModificationEvent modState : modificationQueue.getModificationEvents()) {

                if (getUuid().equals(modState.getUuid())) {

                    versionChanged = versionChanged || modState.getRemovedProperties().containsKey(versionKey)
                            || modState.getModifiedProperties().containsKey(versionKey)
                            || modState.getNewProperties().containsKey(versionKey);
                }
            }

            if (versionChanged) {

                for (AbstractMinifiedFile minifiedFile : targets) {

                    try {
                        minifiedFile.minify();
                    } catch (IOException ex) {
                        logger.warn("Could not automatically update minification target: "
                                .concat(minifiedFile.getName()), ex);
                    }
                }
            }
        }
    }

    static InputStream getInputStream(final File thisFile) {

        final java.io.File fileOnDisk = thisFile.getFileOnDisk();

        try {

            final FileInputStream fis = new FileInputStream(fileOnDisk);
            final SecurityContext securityContext = thisFile.getSecurityContext();

            if (thisFile.isTemplate()) {

                boolean editModeActive = false;
                if (securityContext.getRequest() != null) {

                    final String editParameter = securityContext.getRequest().getParameter("edit");
                    if (editParameter != null) {

                        editModeActive = !RenderContext.EditMode.NONE.equals(RenderContext.editMode(editParameter));
                    }
                }

                if (!editModeActive) {

                    final String content = IOUtils.toString(fis, "UTF-8");

                    // close input stream here
                    fis.close();

                    try {

                        final String result = Scripting.replaceVariables(new ActionContext(securityContext),
                                thisFile, content);

                        String encoding = "UTF-8";

                        final String cType = getContentType();
                        if (cType != null) {

                            final String charset = StringUtils.substringAfterLast(cType, "charset=").trim()
                                    .toUpperCase();
                            try {
                                if (!"".equals(charset) && Charset.isSupported(charset)) {
                                    encoding = charset;
                                }
                            } catch (IllegalCharsetNameException ice) {
                                logger.warn("Charset is not supported '{}'. Using 'UTF-8'", charset);
                            }
                        }

                        return IOUtils.toInputStream(result, encoding);

                    } catch (Throwable t) {

                        logger.warn("Scripting error in {}:\n{}\n{}", thisFile.getUuid(), content, t.getMessage());
                    }
                }
            }

            return fis;

        } catch (IOException ex) {
            logger.warn("Unable to open input stream for {}: {}", fileOnDisk.getPath(), ex.getMessage());
        }

        return null;
    }

    static FileOutputStream getOutputStream(final File thisFile) {
        return thisFile.getOutputStream(true, false);
    }

    static FileOutputStream getOutputStream(final File thisFile, final boolean notifyIndexerAfterClosing,
            final boolean append) {

        if (thisFile.isTemplate()) {

            logger.error("File is in template mode, no write access allowed: {}", thisFile.getPath());
            return null;
        }

        try {

            // Return file output stream and save checksum and size after closing
            return new ClosingFileOutputStream(thisFile, append, notifyIndexerAfterClosing);

        } catch (IOException e) {
            logger.error("Unable to open file output stream for {}: {}", thisFile.getPath(), e.getMessage());
        }

        return null;

    }

    static java.io.File getFileOnDisk(final File thisFile) {
        return thisFile.getFileOnDisk(true);
    }

    static java.io.File getFileOnDisk(final File thisFile, final boolean create) {

        final Folder parentFolder = thisFile.getParent();
        if (parentFolder != null) {

            return parentFolder.getFileOnDisk(thisFile, "", create);

        } else {

            return AbstractFile.defaultGetFileOnDisk(thisFile, create);
        }
    }

    static Map<String, Object> getFirstLines(final File thisFile, final Map<String, Object> parameters) {

        final Map<String, Object> result = new LinkedHashMap<>();
        final int num = File.getNumberOrDefault(parameters, "num", 3);
        final LineAndSeparator ls = File.getFirstLines(thisFile, num);
        final String separator = ls.getSeparator();

        switch (separator) {

        case "\n":
            result.put("separator", "LF");
            break;

        case "\r":
            result.put("separator", "CR");
            break;

        case "\r\n":
            result.put("separator", "CR+LF");
            break;
        }

        result.put("lines", ls.getLine());

        return result;
    }

    static Map<String, Object> getCSVHeaders(final File thisFile, final Map<String, Object> parameters)
            throws FrameworkException {

        if ("text/csv".equals(thisFile.getContentType())) {

            final Map<String, Object> map = new LinkedHashMap<>();
            final Function<Object, Object> func = Functions.get("get_csv_headers");

            if (func != null) {

                try {

                    final Object[] sources = new Object[4];
                    String delimiter = ";";
                    String quoteChar = "\"";
                    String recordSeparator = "\n";

                    if (parameters != null) {

                        if (parameters.containsKey("delimiter")) {
                            delimiter = parameters.get("delimiter").toString();
                        }
                        if (parameters.containsKey("quoteChar")) {
                            quoteChar = parameters.get("quoteChar").toString();
                        }
                        if (parameters.containsKey("recordSeparator")) {
                            recordSeparator = parameters.get("recordSeparator").toString();
                        }
                    }

                    // allow web-friendly specification of line endings
                    switch (recordSeparator) {

                    case "CR+LF":
                        recordSeparator = "\r\n";
                        break;

                    case "CR":
                        recordSeparator = "\r";
                        break;

                    case "LF":
                        recordSeparator = "\n";
                        break;

                    case "TAB":
                        recordSeparator = "\t";
                        break;
                    }

                    sources[0] = File.getFirstLines(thisFile, 1).getLine();
                    sources[1] = delimiter;
                    sources[2] = quoteChar;
                    sources[3] = recordSeparator;

                    map.put("headers", func.apply(new ActionContext(thisFile.getSecurityContext()), null, sources));

                } catch (UnlicensedScriptException ex) {

                    logger.warn("CSV module is not available.");
                }
            }

            return map;

        } else {

            throw new FrameworkException(400, "File format is not CSV");
        }
    }

    static void doCSVImport(final File thisFile, final Map<String, Object> parameters) throws FrameworkException {

        CSVFileImportJob job = new CSVFileImportJob(thisFile, thisFile.getSecurityContext().getUser(false),
                parameters, thisFile.getSecurityContext().getContextStore());
        JobQueueManager.getInstance().addJob(job);

    }

    static String getXMLStructure(final File thisFile) throws FrameworkException {

        final String contentType = thisFile.getContentType();

        if ("text/xml".equals(contentType) || "application/xml".equals(contentType)) {

            try (final Reader input = new InputStreamReader(thisFile.getInputStream())) {

                final XMLStructureAnalyzer analyzer = new XMLStructureAnalyzer(input);
                final Gson gson = new GsonBuilder().setPrettyPrinting().create();

                return gson.toJson(analyzer.getStructure(100));

            } catch (XMLStreamException | IOException ex) {
                ex.printStackTrace();
            }
        }

        return null;
    }

    static void doXMLImport(final File thisFile, final Map<String, Object> config) throws FrameworkException {

        XMLFileImportJob job = new XMLFileImportJob(thisFile, thisFile.getSecurityContext().getUser(false), config,
                thisFile.getSecurityContext().getContextStore());
        JobQueueManager.getInstance().addJob(job);

    }

    /**
     * Returns the Folder entity for the current working directory,
     * or the user's home directory as a fallback.
     * @return
     */
    static Folder getCurrentWorkingDir(final File thisFile) {

        final Principal _owner = thisFile.getProperty(owner);
        Folder workingOrHomeDir = null;

        if (_owner != null && _owner instanceof User) {

            final User user = (User) _owner;

            workingOrHomeDir = user.getWorkingDirectory();
            if (workingOrHomeDir == null) {

                workingOrHomeDir = user.getHomeDirectory();
            }
        }

        return workingOrHomeDir;
    }

    static int getNumberOrDefault(final Map<String, Object> data, final String key, final int defaultValue) {

        final Object value = data.get(key);

        if (value != null) {

            // try number
            if (value instanceof Number) {
                return ((Number) value).intValue();
            }

            // try string
            if (value instanceof String) {
                try {
                    return Integer.valueOf((String) value);
                } catch (NumberFormatException nex) {
                }
            }
        }

        return defaultValue;
    }

    static LineAndSeparator getFirstLines(final File thisFile, final int num) {

        final StringBuilder lines = new StringBuilder();
        int separator[] = new int[10];
        int separatorLength = 0;

        try (final BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(), "utf-8"))) {

            int[] buf = new int[10010];

            int ch = reader.read();
            int i = 0;
            int l = 0;

            // break on file end or if max char or line count is reached
            while (ch != -1 && i < 10000 && l < num) {

                switch (ch) {

                // CR
                case 13:

                    // take only first line ending separator into account
                    if (separator.length < 1) {

                        // collect first separator char
                        separator[separatorLength++] = ch;
                    }

                    // check next char only in case of CR
                    ch = reader.read();

                    // next char is LF ?
                    if (ch == 10) {

                        // CR + LF as line ending, collect second separator char
                        separator[separatorLength++] = ch;

                    } else {

                        // CR only - do nothing
                    }

                    // append LF as line ending for display purposes
                    buf[i++] = '\n';

                    // add line to output
                    lines.append(new String(buf, 0, i));

                    // reset buffer
                    buf = new int[10010];
                    i = 0;
                    l++;

                    break;

                // LF
                case 10:

                    // take only first line ending separator into account
                    if (separator.length < 1) {

                        // collect first separator char
                        separator[separatorLength++] = ch;
                    }

                    // must be LF only because two LF have to be ignored as empty line
                    buf[i++] = '\n';

                    // add line to output
                    lines.append(new String(buf, 0, i));

                    // reset buffer
                    buf = new int[10010];
                    i = 0;
                    l++;

                    break;

                default:

                    // no CR, no LF: Just add char
                    buf[i++] = ch;
                    break;
                }

                ch = reader.read();
            }

            lines.append(new String(buf, 0, i));

        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return new LineAndSeparator(lines.toString(), new String(separator, 0, separatorLength));
    }

    // ----- interface JavaScriptSource -----
    static String getJavascriptLibraryCode(final File thisFile) {

        try (final InputStream is = thisFile.getInputStream()) {

            return IOUtils.toString(new InputStreamReader(is));

        } catch (IOException ioex) {
            logger.warn("", ioex);
        }

        return null;
    }

    // ----- CMIS support -----
    static boolean isImmutable(final File thisFile) {

        final Principal _owner = thisFile.getOwnerNode();
        if (_owner != null) {

            return !_owner.isGranted(Permission.write, thisFile.getSecurityContext());
        }

        return true;
    }

    // ----- interface Favoritable -----
    static String getFavoriteContent(final File thisFile) {

        try (final InputStream is = thisFile.getInputStream()) {

            return IOUtils.toString(is, "utf-8");

        } catch (IOException ioex) {
            logger.warn("Unable to get favorite content from {}: {}", thisFile, ioex.getMessage());
        }

        return null;
    }

    static void setFavoriteContent(final File thisFile, final String content) throws FrameworkException {

        try (final OutputStream os = thisFile.getOutputStream(true, false)) {

            IOUtils.write(content, os, Charset.defaultCharset());
            os.flush();

        } catch (IOException ioex) {
            logger.warn("Unable to set favorite content from {}: {}", thisFile, ioex.getMessage());
        }
    }

    class LineAndSeparator {

        private String line = null;
        private String separator = null;

        public LineAndSeparator(final String line, final String separator) {
            this.line = line;
            this.separator = separator;
        }

        public String getLine() {
            return line;
        }

        public String getSeparator() {
            return separator;
        }
    }
}