| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.netbeans.modules.properties; |
| |
| import org.openide.util.UserCancelException; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.io.ObjectInputStream; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.TreeSet; |
| import java.util.logging.Logger; |
| import org.openide.cookies.OpenCookie; |
| import org.openide.cookies.SaveCookie; |
| |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.loaders.DataFolder; |
| import org.openide.loaders.DataNode; |
| import org.openide.loaders.DataObject; |
| import org.openide.loaders.DataObjectExistsException; |
| import org.openide.loaders.DataObjectNotFoundException; |
| import org.openide.loaders.MultiDataObject; |
| import org.openide.nodes.Children; |
| import org.openide.nodes.CookieSet; |
| import org.openide.nodes.Node; |
| import org.openide.util.Exceptions; |
| import org.openide.util.Lookup; |
| import org.openide.util.WeakListeners; |
| import static java.util.logging.Level.FINER; |
| import org.openide.filesystems.MIMEResolver; |
| |
| |
| /** |
| * Object that provides main functionality for properties data loader. |
| * Represents set of .properties files with same basic name (name without locale postfix). |
| * |
| * @author Ian Formanek |
| */ |
| @MIMEResolver.ExtensionRegistration( |
| displayName="#PropertiesResolver", |
| extension="properties", |
| mimeType="text/x-properties", |
| position=120 |
| ) |
| public final class PropertiesDataObject extends MultiDataObject implements CookieSet.Factory { |
| |
| /** Generated Serialized Version UID. */ |
| static final long serialVersionUID = 4795737295255253334L; |
| |
| static final Logger LOG = Logger.getLogger(PropertiesDataObject.class.getName()); |
| |
| /** Structural view of the dataobject */ |
| private transient BundleStructure bundleStructure; |
| |
| /** Open support for this data object. Provides editable table view on bundle. */ |
| private transient PropertiesOpen openSupport; |
| |
| /** Lock used for synchronization of <code>openSupport</code> instance creation */ |
| private final transient Object OPEN_SUPPORT_LOCK = new Object(); |
| |
| // Hack due having lock on secondaries, can't override handleCopy, handleMove at all. |
| /** Suffix used by copying/moving dataObject. */ |
| private transient String pasteSuffix; |
| |
| /** */ |
| private Lookup lookup; |
| |
| |
| /** |
| * Constructs a <code>PropertiesDataObject</code> for a specified |
| * primary file. |
| * |
| * @param primaryFile primary file to creata a data object for |
| * @param loader data loader which recognized the primary file |
| * @exception org.openide.loaders.DataObjectExistsException |
| * if another <code>DataObject</code> already exists |
| * for the specified file |
| */ |
| public PropertiesDataObject(final FileObject primaryFile, |
| final PropertiesDataLoader loader) |
| throws DataObjectExistsException { |
| super(primaryFile, loader); |
| // use editor support |
| initialize(); |
| } |
| |
| /** |
| */ |
| PropertiesEncoding getEncoding() { |
| return ((PropertiesDataLoader) getLoader()).getEncoding(); |
| } |
| |
| @Override |
| protected int associateLookup() { |
| return 1; |
| } |
| |
| /** Initializes the object. Used by construction and deserialized. */ |
| private void initialize() { |
| bundleStructure = null; |
| Class<? extends Node.Cookie>[] arr = (Class<Node.Cookie>[]) new Class[2]; |
| arr[0] = PropertiesOpen.class; |
| arr[1] = PropertiesEditorSupport.class; |
| getCookieSet().add(arr, this); |
| getCookieSet().assign(PropertiesEncoding.class, getEncoding()); |
| } |
| |
| /** Implements <code>CookieSet.Factory</code> interface method. */ |
| @SuppressWarnings("unchecked") |
| public <T extends Node.Cookie> T createCookie(Class<T> clazz) { |
| if(clazz.isAssignableFrom(PropertiesOpen.class)) { |
| return (T) getOpenSupport(); |
| } else if(clazz.isAssignableFrom(PropertiesEditorSupport.class)) { |
| return (T) ((PropertiesFileEntry)getPrimaryEntry()).getPropertiesEditor(); |
| } else { |
| return null; |
| } |
| } |
| |
| // Accessibility from PropertiesOpen: |
| CookieSet getCookieSet0() { |
| return getCookieSet(); |
| } |
| |
| @Override |
| protected FileObject handleRename(String name) throws IOException { |
| boolean baseNameChanged = false; |
| BundleStructure oldStructure = (MultiBundleStructure) bundleStructure; |
| FileObject fo = this.getPrimaryFile(); |
| PropertiesOpen openCookie = (PropertiesOpen) getCookie(OpenCookie.class); |
| if (openCookie != null) { |
| openCookie.removeModifiedListener(this); |
| openCookie.close(); |
| } |
| if (bundleStructure != null && bundleStructure.getEntryCount()>1) { |
| if (!Util.getBaseName(name).equals(Util.getBaseName(this.getName()))) { |
| //This means that new OpenCookie should be created |
| baseNameChanged = true; |
| bundleStructure = null; |
| openSupport = null; |
| } |
| } |
| try { |
| return super.handleRename(name); |
| } finally { |
| if (baseNameChanged && oldStructure!=null && oldStructure.getEntryCount()>1) { |
| oldStructure.updateEntries(); |
| oldStructure.notifyOneFileChanged(fo); |
| } |
| bundleStructure = null; |
| openSupport = null; |
| } |
| } |
| |
| |
| /** Copies primary and secondary files to new folder. |
| * Overrides superclass method. |
| * @param df the new folder |
| * @return data object for the new primary |
| * @throws IOException if there was a problem copying |
| * @throws UserCancelException if the user cancelled the copy */ |
| @Override |
| protected synchronized DataObject handleCopy(DataFolder df) throws IOException { |
| if (LOG.isLoggable(FINER)) { |
| LOG.finer("handleCopy(" //NOI18N |
| + FileUtil.getFileDisplayName(df.getPrimaryFile()) + ')'); |
| } |
| try { |
| // pasteSuffix = createPasteSuffix(df); |
| |
| return super.handleCopy(df); |
| } finally { |
| pasteSuffix = null; |
| bundleStructure = null; |
| } |
| } |
| |
| @Override |
| protected void handleDelete() throws IOException { |
| if (LOG.isLoggable(FINER)) { |
| LOG.finer("handleDelete()"); |
| } |
| PropertiesOpen openCookie = (PropertiesOpen) getCookie(OpenCookie.class); |
| if (openCookie != null) { |
| openCookie.removeModifiedListener(this); |
| // openCookie.close(); |
| bundleStructure = null; |
| openSupport = null; |
| } |
| super.handleDelete(); |
| } |
| |
| |
| /** Moves primary and secondary files to a new folder. |
| * Overrides superclass method. |
| * @param df the new folder |
| * @return the moved primary file object |
| * @throws IOException if there was a problem moving |
| * @throws UserCancelException if the user cancelled the move */ |
| @Override |
| protected FileObject handleMove(DataFolder df) throws IOException { |
| if (LOG.isLoggable(FINER)) { |
| LOG.finer("handleMove(" //NOI18N |
| + FileUtil.getFileDisplayName(df.getPrimaryFile()) + ')'); |
| } |
| |
| BundleStructure oldStructure = (MultiBundleStructure) bundleStructure; |
| FileObject fo = this.getPrimaryFile(); |
| // a simple fix of issue #92195 (impossible to save a moved prop. file): |
| SaveCookie saveCookie = getCookie(SaveCookie.class); |
| if (saveCookie != null) { |
| saveCookie.save(); |
| } |
| PropertiesOpen openCookie = (PropertiesOpen) getCookie(OpenCookie.class); |
| if (openCookie != null) { |
| openCookie.removeModifiedListener(this); |
| openCookie.close(); |
| bundleStructure = null; |
| openSupport = null; |
| } |
| // getCookieSet().remove(openCookie); |
| try { |
| // pasteSuffix = createPasteSuffix(df); |
| |
| return super.handleMove(df); |
| } finally { |
| //Here data object has old path still but in invalid state |
| if (oldStructure!=null && oldStructure.getEntryCount()>1) { |
| oldStructure.updateEntries(); |
| oldStructure.notifyOneFileChanged(fo); |
| } |
| pasteSuffix = null; |
| bundleStructure = null; |
| openSupport = null; |
| } |
| } |
| |
| /** Gets suffix used by entries by copying/moving. */ |
| String getPasteSuffix() { |
| return pasteSuffix; |
| } |
| |
| /** Only accessible method, it is necessary to call MultiDataObject's method |
| * from this package. |
| */ |
| void removeSecondaryEntry2(Entry fe) { |
| if (LOG.isLoggable(FINER)) { |
| LOG.finer("removeSecondaryEntry2(Entry " //NOI18N |
| + FileUtil.getFileDisplayName(fe.getFile()) + ')'); |
| } |
| removeSecondaryEntry (fe); |
| } |
| |
| /** Creates new name for this instance when moving/copying to new folder destination. |
| * @param folder new folder destination. */ |
| private String createPasteSuffix(DataFolder folder) { |
| String basicName = getPrimaryFile().getName(); |
| |
| DataObject[] children = folder.getChildren(); |
| |
| |
| // Repeat until there is not such file name. |
| for(int i = 0; ; i++) { |
| String newName; |
| |
| if (i == 0) { |
| newName = basicName; |
| } else { |
| newName = basicName + i; |
| } |
| boolean exist = false; |
| |
| for(int j = 0; j < children.length; j++) { |
| if(children[j] instanceof PropertiesDataObject && newName.equals(children[j].getName())) { |
| exist = true; |
| break; |
| } |
| } |
| |
| if(!exist) { |
| if (i == 0) { |
| return ""; // NOI18N |
| } else { |
| return "" + i; // NOI18N |
| } |
| } |
| } |
| } |
| |
| /** Returns open support. It's used by all subentries as open support too. */ |
| public PropertiesOpen getOpenSupport() { |
| if (openSupport == null) { |
| openSupport = ((MultiBundleStructure)getBundleStructure()).getOpenSupport(); |
| if (this.isValid()) |
| openSupport.addDataObject(this); |
| } |
| return openSupport; |
| } |
| |
| /** Updates modification status of this dataobject from its entries. */ |
| void updateModificationStatus() { |
| LOG.finer("updateModificationStatus()"); //NOI18N |
| boolean modif = false; |
| if (((PresentableFileEntry)getPrimaryEntry()).isModified()) |
| modif = true; |
| else { |
| for (Iterator it = secondaryEntries().iterator(); it.hasNext(); ) { |
| if (((PresentableFileEntry)it.next()).isModified()) { |
| modif = true; |
| break; |
| } |
| } |
| } |
| |
| super.setModified(modif); |
| } |
| |
| /** Provides node that should represent this data object. When a node for representation |
| * in a parent is requested by a call to getNode (parent) it is the exact copy of this node |
| * with only parent changed. This implementation creates instance |
| * <CODE>DataNode</CODE>. |
| * <P> |
| * This method is called only once. |
| * |
| * @return the node representation for this data object |
| * @see DataNode |
| */ |
| @Override |
| protected Node createNodeDelegate () { |
| return new PropertiesDataNode(this, getLookup()); |
| } |
| |
| Children getChildren() { |
| return new PropertiesChildren(); |
| } |
| |
| //TODO XXX Now it is always false |
| boolean isMultiLocale() { |
| return secondaryEntries().size() > 0; |
| } |
| |
| /** |
| * Find existing BundleStructure instance. |
| * @return BundleStructure from first DataObject with the same base name or null |
| */ |
| protected synchronized BundleStructure findBundleStructure() { |
| PropertiesDataObject dataObject = null; |
| BundleStructure structure; |
| try { |
| dataObject = Util.findPrimaryDataObject(this); |
| } catch (DataObjectNotFoundException doe) { |
| Exceptions.printStackTrace(doe); |
| } |
| if(this == dataObject) { |
| structure = new MultiBundleStructure(this); |
| return structure; |
| } else { |
| return dataObject.getBundleStructure(); |
| } |
| } |
| |
| /** Getter for bundleStructure property */ |
| protected BundleStructure getBundleStructureOrNull () { |
| return bundleStructure; |
| } |
| |
| /** Returns a structural view of this data object */ |
| public BundleStructure getBundleStructure() { |
| if (bundleStructure==null) { |
| try { |
| bundleStructure = Util.findBundleStructure(this.getPrimaryFile(), this.getPrimaryFile().getParent(), Util.getBaseName(this.getName())); |
| if (bundleStructure == null) |
| bundleStructure = new MultiBundleStructure(this); |
| bundleStructure.updateEntries(); |
| } catch (DataObjectNotFoundException ex) { |
| Exceptions.printStackTrace(ex); |
| return null; |
| } |
| } |
| return bundleStructure; |
| } |
| |
| protected void setBundleStructure(BundleStructure structure) { |
| if (bundleStructure != structure) { |
| bundleStructure = structure; |
| } |
| } |
| |
| /** Comparator used for ordering secondary files, works over file names */ |
| public static Comparator<String> getSecondaryFilesComparator() { |
| return new KeyComparator(); |
| } |
| |
| /** |
| */ |
| void fireNameChange() { |
| LOG.finer("fireNameChange()"); //NOI18N |
| firePropertyChange(PROP_NAME, null, null); |
| } |
| |
| /** Deserialization. */ |
| private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { |
| in.defaultReadObject(); |
| initialize(); |
| } |
| |
| |
| /** Children of this <code>PropertiesDataObject</code>. */ |
| private class PropertiesChildren extends Children.Keys<String> { |
| |
| /** Listens to changes on the dataobject */ |
| private PropertyChangeListener propertyListener = null; |
| private PropertyChangeListener weakPropListener = null; |
| |
| |
| /** Constructor.*/ |
| PropertiesChildren() { |
| super(); |
| } |
| |
| |
| /** Sets all keys in the correct order */ |
| protected void mySetKeys() { |
| TreeSet<String> newKeys = new TreeSet<String>(new Comparator<String>() { |
| public int compare(String o1, String o2) { |
| if (o1 == o2) { |
| return 0; |
| } |
| if (o1 == null) { |
| return -1; |
| } |
| if (o2 == null) { |
| return 1; |
| } |
| return o1.compareTo(o2); |
| } |
| }); |
| |
| newKeys.add(getPrimaryEntry().getFile().getName()); |
| |
| for (Entry entry : secondaryEntries()) { |
| newKeys.add(entry.getFile().getName()); |
| } |
| |
| setKeys(newKeys); |
| } |
| |
| /** Called to notify that the children has been asked for children |
| * after and that they should set its keys. Overrides superclass method. */ |
| @Override |
| protected void addNotify () { |
| mySetKeys(); |
| |
| // listener |
| if(propertyListener == null) { |
| propertyListener = new PropertyChangeListener () { |
| public void propertyChange(PropertyChangeEvent evt) { |
| if(PROP_FILES.equals(evt.getPropertyName())) { |
| if (isMultiLocale()) { |
| mySetKeys(); |
| } else { |
| // These children are only used for two or more locales. |
| // If only default locale is left, disconnect the listener. |
| // This children object is going to be removed, but |
| // for some reason it causes problems setting new keys here. |
| if (propertyListener != null) { |
| PropertiesDataObject.this.removePropertyChangeListener(weakPropListener); |
| propertyListener = null; |
| } |
| } |
| } |
| } |
| }; |
| weakPropListener = WeakListeners.propertyChange(propertyListener, PropertiesDataObject.this); |
| PropertiesDataObject.this.addPropertyChangeListener(weakPropListener); |
| } |
| } |
| |
| /** Called to notify that the children has lost all of its references to |
| * its nodes associated to keys and that the keys could be cleared without |
| * affecting any nodes (because nobody listens to that nodes). |
| * Overrides superclass method. */ |
| @Override |
| protected void removeNotify () { |
| setKeys(new ArrayList<String>()); |
| } |
| |
| /** Creates nodes for specified key. Implements superclass abstract method. */ |
| protected Node[] createNodes(String entryName) { |
| if (entryName == null) { |
| return null; |
| } |
| |
| PropertiesFileEntry entry = (PropertiesFileEntry)getPrimaryEntry(); |
| |
| if(entryName.equals(entry.getFile().getName())) { |
| return new Node[] {entry.getNodeDelegate()}; |
| } |
| for(Iterator<Entry> it = secondaryEntries().iterator();it.hasNext();) { |
| entry = (PropertiesFileEntry)it.next(); |
| |
| if (entryName.equals(entry.getFile().getName())) { |
| return new Node[] {entry.getNodeDelegate()}; |
| } |
| } |
| |
| return null; |
| } |
| |
| } // End of class PropertiesChildren. |
| |
| } |