/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.nodeEditor.selection; import jetbrains.mps.module.ReloadableModule; import jetbrains.mps.nodeEditor.cells.DefaultCellInfo; import jetbrains.mps.openapi.editor.EditorComponent; import jetbrains.mps.openapi.editor.cells.CellInfo; import jetbrains.mps.openapi.editor.selection.Selection; import jetbrains.mps.openapi.editor.selection.SelectionInfo; import jetbrains.mps.openapi.editor.selection.SelectionStoreException; import jetbrains.mps.smodel.ModelAccessHelper; import jetbrains.mps.smodel.ModuleRepositoryFacade; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; public class SelectionInfoImpl implements SelectionInfo { private static final Logger LOG = LogManager.getLogger(SelectionInfoImpl.class); private static final String CLASS_NAME_ATTRIBUTE = "className"; private static final String MODULE_ID_ATTRIBUTE = "moduleID"; private static final String CELL_INFO_ELEMENT_NAME = "cellInfo"; private static final String PROPERTY_ELEMENT_NAME = "property"; private static final String PROPERTY_NAME_ATTRIBUTE = "name"; private static final String PROPERTY_VALUE_ATTRIBUTE = "value"; private String mySelectionClassName; private String myModuleID; private Map<String, String> myProperties = new HashMap<String, String>(); private DefaultCellInfo myCellInfo = null; public SelectionInfoImpl(Element element) { mySelectionClassName = element.getAttributeValue(CLASS_NAME_ATTRIBUTE); myModuleID = element.getAttributeValue(MODULE_ID_ATTRIBUTE); for (Object childElement : element.getChildren(PROPERTY_ELEMENT_NAME)) { Element entry = (Element) childElement; String name = entry.getAttributeValue(PROPERTY_NAME_ATTRIBUTE); assert name != null; String value = entry.getAttributeValue(PROPERTY_VALUE_ATTRIBUTE); assert value != null; myProperties.put(name, value); } Element cellInfoElement = element.getChild(CELL_INFO_ELEMENT_NAME); if (cellInfoElement != null) { myCellInfo = DefaultCellInfo.loadFrom(cellInfoElement); } } public SelectionInfoImpl(@NotNull String selectionClassName, @Nullable SModuleReference moduleID) { this(selectionClassName); myModuleID = moduleID == null ? null : PersistenceFacade.getInstance().asString(moduleID); } public SelectionInfoImpl(@NotNull String selectionClassName) { mySelectionClassName = selectionClassName; } public Map<String, String> getPropertiesMap() { return myProperties; } public void setCellInfo(CellInfo cellInfo) throws SelectionStoreException { if (!(cellInfo instanceof DefaultCellInfo)) { throw new SelectionStoreException("CellInfo is different from DefaultCellInfo: " + cellInfo); } myCellInfo = (DefaultCellInfo) cellInfo; } @Override public Selection createSelection(EditorComponent editorComponent) { try { Class<?> selectionClass; if (myModuleID != null) { SRepository repo = editorComponent.getEditorContext().getRepository(); // XXX I have no idea whether there's model read access when #createSelection is invoked, rather take one. ReloadableModule reloadableModule = new ModelAccessHelper(repo).runReadAction(() -> { SModule module; try { SModuleReference mr = PersistenceFacade.getInstance().createModuleReference(myModuleID); module = mr.resolve(repo); } catch (IllegalArgumentException ex) { // fallback, perhaps, it's an old selection, where just module name has been stored // TODO remove this fallback once 3.5 is out module = new ModuleRepositoryFacade(repo).getModuleByName(myModuleID); } if (module == null) { LOG.error("Specified selection class module was not found by ID: " + myModuleID); return null; } if (!(module instanceof ReloadableModule)) { LOG.error(String.format("Module %s of specified selection class (%s) can not load classes", myModuleID, mySelectionClassName)); return null; } return (ReloadableModule) module; }); if (reloadableModule == null) { return null; } // I know it's odd to access module outside ot model read (although the module is likely deployed and shall not // get disposed unexpectedly). Just don't want to refactor the a lot (exception handling for both if/else cases). selectionClass = reloadableModule.getClass(mySelectionClassName); } else { selectionClass = getClass().getClassLoader().loadClass(mySelectionClassName); } if (!Selection.class.isAssignableFrom(selectionClass)) { LOG.error("Serialized selection class: " + mySelectionClassName + " is not a subclass of " + Selection.class.getName()); return null; } Constructor<Selection> constructor = ((Class<Selection>) selectionClass).getConstructor(EditorComponent.class, Map.class, CellInfo.class); return constructor.newInstance(editorComponent, myProperties, myCellInfo); } catch (ClassNotFoundException | NoSuchMethodException e) { LOG.error(null, e); return null; } catch (InvocationTargetException e) { if (e.getTargetException() instanceof SelectionRestoreException) { /** * Skipping this exception because it is indicating that selection was not restored due to some changes in * associated model(s). * * For example: element that was indicated as selected before is not in a model anymore (was deleter). */ return null; } LOG.error(null, e); return null; } catch (InstantiationException e) { LOG.error(null, e); return null; } catch (IllegalAccessException e) { LOG.error(null, e); return null; } } public void persistToXML(Element element) { element.setAttribute(CLASS_NAME_ATTRIBUTE, mySelectionClassName); if (myModuleID != null) { element.setAttribute(MODULE_ID_ATTRIBUTE, myModuleID); } for (Entry<String, String> propertyEntry : myProperties.entrySet()) { Element propertyElement = new Element(PROPERTY_ELEMENT_NAME); propertyElement.setAttribute(PROPERTY_NAME_ATTRIBUTE, propertyEntry.getKey()); propertyElement.setAttribute(PROPERTY_VALUE_ATTRIBUTE, propertyEntry.getValue()); element.addContent(propertyElement); } if (myCellInfo != null) { Element cellInfoElement = new Element(CELL_INFO_ELEMENT_NAME); myCellInfo.saveTo(cellInfoElement); element.addContent(cellInfoElement); } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SelectionInfoImpl that = (SelectionInfoImpl) o; if (!mySelectionClassName.equals(that.mySelectionClassName)) { return false; } if (myCellInfo != null ? !myCellInfo.equals(that.myCellInfo) : that.myCellInfo != null) { return false; } if (!myProperties.equals(that.myProperties)) { return false; } return true; } @Override public int hashCode() { int result = mySelectionClassName.hashCode(); result = 31 * result + myProperties.hashCode(); result = 31 * result + (myCellInfo != null ? myCellInfo.hashCode() : 0); return result; } public static class Util { public static int getIntProperty(Map<String, String> properties, String propertyName) throws SelectionStoreException { String propertyValue = properties.get(propertyName); if (propertyValue == null) { throw new SelectionStoreException("Cannot load int property - property value was not specified for propertyName = " + propertyName); } try { return Integer.parseInt(propertyValue); } catch (NumberFormatException e) { throw new SelectionStoreException("Unable to parse integer position value: " + propertyValue); } } public static boolean getBooleanProperty(Map<String, String> properties, String propertyName) throws SelectionStoreException { String propertyValue = properties.get(propertyName); if (propertyValue == null) { throw new SelectionStoreException("Cannot load boolean property - property value was not specified for propertyName = " + propertyName); } return Boolean.parseBoolean(propertyValue); } public static Enum getEnumProperty(Map<String, String> properties, String propertyName, Class<? extends Enum> enumClass, Enum defaultPropertyValue) throws SelectionStoreException { String propertyValue = properties.get(propertyName); if (propertyValue == null) { return defaultPropertyValue; // throw new SelectionStoreException("Cannot load enum property - property value was not specified for propertyName = " + propertyName); } try { return Enum.valueOf(enumClass, propertyValue); } catch (IllegalArgumentException ex) { throw new SelectionStoreException("Invalid enum literal name specified: " + propertyValue + " for enum: " + enumClass.getCanonicalName()); } } } }