/* license-start * * Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 3. * * This program 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 General Public License for more details, at <http://www.gnu.org/licenses/>. * * Contributors: * Crispico - Initial API and implementation * * license-end */ package org.flowerplatform.editor.text.remote; import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Collections; import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.flowerplatform.communication.CommunicationPlugin; import org.flowerplatform.communication.channel.CommunicationChannel; import org.flowerplatform.communication.stateful_service.StatefulServiceInvocationContext; import org.flowerplatform.editor.EditorPlugin; import org.flowerplatform.editor.remote.EditableResource; import org.flowerplatform.editor.remote.EditableResourceClient; import org.flowerplatform.editor.remote.FileBasedEditorStatefulService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Cristi * @author Mariana * * */ public class TextEditorStatefulService extends FileBasedEditorStatefulService { // /** // * @see #createRegexConfiguration() // * // */ // protected RegexConfiguration regexConfiguration; /** * */ private static final Logger logger = LoggerFactory.getLogger(TextEditorStatefulService.class); public static final String CLIENT_KEYSTROKE_AGGREGATION_INTERVAL = "client.keystroke.aggregation.interval"; //milliseconds // static { // FlowerWebProperties.INSTANCE.addProperty(new AddIntegerProperty(CLIENT_KEYSTROKE_AGGREGATION_INTERVAL, "3000")); // } public TextEditorStatefulService() { createRegexConfiguration(); CommunicationPlugin.getInstance().getCommunicationChannelManager().addWebCommunicationLifecycleListener(this); } /** * */ @Override protected boolean areLocalUpdatesAppliedImmediately() { return true; } /** * */ @Override protected EditableResource createEditableResourceInstance() { return new TextEditableResource(); } /** * If there is an error at content loading, throw {@link FileNotFoundException} if the file * does not exist. * * Note: testing that the file exists will fail before loading the contents, because the resource * is not refreshed and thus may be out of sync with the file system. The test is safe to do after * the content is loaded, because loading also triggers refreshing. * * @author Mariana * * */ @Override protected void loadEditableResource(StatefulServiceInvocationContext context, EditableResource editableResource) { super.loadEditableResource(context, editableResource); TextEditableResource er = (TextEditableResource) editableResource; try { er.setFileContent(new StringBuffer(IOUtils.toString(EditorPlugin.getInstance().getFileAccessController().getContent(er.getFile())))); } catch (IOException e) { logger.error("Could not set file content for " + er); } // See class comment for purpose of eoln delimiter converting. String replacedEolnDelimiter = Utilities.makeFlexCompatibleDelimiters(er.getFileContent()); // Inplace replacement of delimiters. er.setReplacedEolnDelimiter(replacedEolnDelimiter); er.setDirty(false); } @Override protected void disposeEditableResource(EditableResource editableResource) { // do nothing } /** * */ @Override public void sendFullContentToClient(EditableResource editableResource, EditableResourceClient client) { TextEditorUpdate update = new TextEditorUpdate(); update.setNewText(((TextEditableResource)editableResource).getFileContent().toString()); sendContentUpdateToClient(editableResource, client, Collections.singleton(update), true); } /** * Writes the content in the {@link TextEditableResource#getFileContent()} to the * text file. Also refreshes the file so the system will detect the changes; * otherwise an exception will be thrown the next time this file is opened. * * <p> NOTE: after saving the file content from the resource still has the delimiters converted. * */ @Override public void doSave(EditableResource editableResource) { TextEditableResource er = (TextEditableResource) editableResource; StringBuffer content = er.getFileContent(); String replacedEolnDelimiter = er.getReplacedEolnDelimiter(); content = Utilities.revertFlexCompatibleDelimiters(content, replacedEolnDelimiter); // Creates a new copy does not affect the editableResource content. EditorPlugin.getInstance().getFileAccessController().setContent(er.getFile(), content.toString()); er.setDirty(false); } /** * */ @Override protected void updateEditableResourceContentAndDispatchUpdates(StatefulServiceInvocationContext context, EditableResource editableResource, Object updatesToApply) { TextEditableResource textEditableResource = (TextEditableResource) editableResource; // update the file contents textEditableResource.setDirty(true); @SuppressWarnings("unchecked") List<TextEditorUpdate> textEditorUpdates = (List<TextEditorUpdate>) updatesToApply; for (TextEditorUpdate update : textEditorUpdates) { if (logger.isTraceEnabled()) { logger.trace("Applying text updates for editorInput = {}: {}", textEditableResource.getEditorInput(), update); } textEditableResource.replaceContent(update.getOffset(), update.getOldTextLength(), update.getNewText()); } for (EditableResourceClient otherClient : textEditableResource.getClients()) { if (!otherClient.getCommunicationChannel().equals(context.getCommunicationChannel())) { if (logger.isTraceEnabled()) { logger.trace("Redispatching text updates to client = {}", otherClient.getCommunicationChannel()); } sendContentUpdateToClient(textEditableResource, otherClient, textEditorUpdates, false); } } } /** * Encode only special characters from name. * * @author Cristina */ @Override public String getFriendlyNameEncoded(String friendlyName) { try { friendlyName = friendlyName.replace(",", URLEncoder.encode(",", "UTF-8")); } catch (UnsupportedEncodingException e) { logger.error("Could not encode using UTF-8 charset : " + friendlyName); } return super.getFriendlyNameEncoded(friendlyName); } /** * Grouping a bunch of atomic handling methods. */ public static class Utilities { /** * Replaces EOLN_RN_DELIMITER and EOLN_R_DELIMITER to EOLN_N_DELIMITER inplace. * @param content the content to work on * @return the replacer delimiter if one existed or null if no replacing was needed. */ public static String makeFlexCompatibleDelimiters(StringBuffer content) { String replacedEolnDelimiter = null; // If end of line different from \n then convert. Multiple eolns do not work. if (content.indexOf(TextEditableResource.EOLN_RN_DELIMITER) >= 0 || content.indexOf(TextEditableResource.EOLN_R_DELIMITER) >= 0) { replacedEolnDelimiter = content.indexOf(TextEditableResource.EOLN_RN_DELIMITER) >= 0 ? TextEditableResource.EOLN_RN_DELIMITER : TextEditableResource.EOLN_R_DELIMITER; content.replace(0, content.length(), content.toString().replace(replacedEolnDelimiter, TextEditableResource.EOLN_N_DELIMITER)); } return replacedEolnDelimiter; } /** * Replaces EOLN_N_DELIMITER delimiter with<code> replacedEolnDelimiter</code> by creating a new copy. * @param content to content work on * @param replacedEolnDelimiter the delimiter to replace the flex compatible delimiter. Can be null and * in this case there will be no replacing resulting in returning the initial content. */ public static StringBuffer revertFlexCompatibleDelimiters(StringBuffer content, String replacedEolnDelimiter) { if (replacedEolnDelimiter == null) return content; else return new StringBuffer(content.toString().replace(TextEditableResource.EOLN_N_DELIMITER, replacedEolnDelimiter)); } } public void selectRange(CommunicationChannel channel, String editableResourcePath, int offset, int length) { TextEditableResource textEditableResource = (TextEditableResource)editableResources.get(editableResourcePath); if (textEditableResource == null) { logger.warn("SelectRange called for " + editableResourcePath + " but it is not opened"); return; // Is not loaded } EditableResourceClient client = textEditableResource.getEditableResourceClientByCommunicationChannel(channel); if (client == null) { logger.warn("SelectRange called for " + editableResourcePath + " but it is not opened for client " + channel); return; // Not loaded by specific client } invokeClientMethod(channel, client.getStatefulClientId(), "selectRange", new Object[] {offset, length}); } /** * Every TextEditorStatefulService specific for a type of editor is responsible for defining * it's regex configuration used in determining for example the index for given attribute/method or the attribute/method * for a given index. * @see #selectRangeFor() * @see #findElementCategoryAndNameForPosition() */ protected void createRegexConfiguration() { } /** * */ public String[] findElementCategoryAndNameForPosition(String editableResourcePath, int position) { // TODO implement return null; } /////////////////////////////////////////////////////////////// // @RemoteInvocation methods /////////////////////////////////////////////////////////////// // /** // * // */ // @RemoteInvocation // public void selectRangeFor(StatefulServiceInvocationContext context, String editableResourcePath, String category, String searchString) { // TextEditableResource editableResource = (TextEditableResource)editableResources.get(editableResourcePath); // if (editableResource == null) // return; // Is not loaded // // RegexProcessingSession searchSession = regexConfiguration.startSession(editableResource.getFileContent().toString()); // int[] searchResult = searchSession.findRangeFor(category, searchString); // // if (searchResult == null) { // if (logger.isTraceEnabled()) // logger.trace(String.format("Could not find %s %s in %s", category, searchString, editableResourcePath)); // return; // The given category and searchString could not be found // } // // int offset = searchResult[0]; // int length = searchResult[1] - searchResult[0]; // // if (logger.isTraceEnabled()) // logger.trace(String.format("Selecting range offset = %s , length = %s , from category = %s with searchString = %s, inside %s ", offset, length, category, searchString, editableResourcePath)); // // selectRange(context.getCommunicationChannel(), editableResourcePath, offset, length); // } // // /** // * By default it tries to navigate to <code>category</code> using the <code>searchString</code> found in the fragment. // * Note: at the moment this method by default knows how to interpret only the first parameter passed to the opening resource. // * // * @author Sorin // */ // public void navigateToFragment(CommunicationChannel channel, String editableResourcePath, String fragment) { // if (fragment == null) // return; // String[] categoryWithSearchString = fragment.split("="); // if (categoryWithSearchString.length != 2) // return; // Could not recognize format must by category=searchString // String category = categoryWithSearchString[0].trim(); // String searchString = categoryWithSearchString[1].trim(); // // selectRangeFor(new StatefulServiceInvocationContext(channel), editableResourcePath, category, searchString); // } // // @Override // protected boolean isResourceChangedNotificationInteresting(IResource resource) { // return resource instanceof IFile; // } }