All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sdmlib.modelspace.ModelSpace Maven / Gradle / Ivy

/*
   Copyright (c) 2015 zuendorf

   Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
   and associated documentation files (the "Software"), to deal in the Software without restriction, 
   including without limitation the rights to use, copy, modify, merge, publish, distribute, 
   sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 
   furnished to do so, subject to the following conditions: 

   The above copyright notice and this permission notice shall be included in all copies or 
   substantial portions of the Software. 

   The Software shall be used for Good, not Evil. 

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
   BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
   DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
 */

package org.sdmlib.modelspace;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.FileTime;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.concurrent.LinkedBlockingQueue;

import org.sdmlib.replication.ChangeEvent;
import org.sdmlib.replication.ChangeEventList;
import org.sdmlib.serialization.PropertyChangeInterface;

import de.uniks.networkparser.IdMap;
import de.uniks.networkparser.SimpleEvent;
import de.uniks.networkparser.interfaces.ObjectCondition;
import de.uniks.networkparser.interfaces.SendableEntity;
import de.uniks.networkparser.interfaces.SendableEntityCreator;
import de.uniks.networkparser.json.JsonArray;
import de.uniks.networkparser.json.JsonObject;
import de.uniks.networkparser.list.AbstractList;
import javafx.application.Platform;

   /**
    * 
    * @see ModelSpaceModel.java
* @see ModelSpaceModel.java
 */
   public  class ModelSpace implements PropertyChangeInterface, ObjectCondition, SendableEntity
{
   public static final String JSONCHGS = ".jsonchgs";

   public enum ApplicationType {StandAlone, JavaFX};
   //==========================================================================

   protected PropertyChangeSupport listeners = new PropertyChangeSupport(this);
   private IdMap idMap;
   private String userName;
   private File modelDir;
   private WatchService watcher;

   private LinkedHashMap fileReaders = new LinkedHashMap();
   private boolean isApplyingChangeMsg;

   //==========================================================================

   public static final String PROPERTY_HISTORY = "history";

   private ChangeEventList history = new ChangeEventList();
   private int historyPos;
   private File logFile;
   private FileWriter logFileWriter;
   private ApplicationType appType;
   private long lastChangeId = 0;
   private Path logPath;

   public LinkedBlockingQueue changeQueue = new LinkedBlockingQueue();

   public ChangeEventList getHistory()
   {
      return this.history;
   }

   public ModelSpace(IdMap idMap, String userName)
   {
      this (idMap, userName, ApplicationType.StandAlone);
   }

   public ModelSpace(IdMap idMap, String userName, ApplicationType appType)
   {
      this.idMap = idMap;
      this.appType = appType;   

      this.userName = userName;

      idMap.with(this);
   }

   public PropertyChangeSupport getPropertyChangeSupport()
   {
      return listeners;
   }

   public boolean addPropertyChangeListener(PropertyChangeListener listener) 
   {
      getPropertyChangeSupport().addPropertyChangeListener(listener);
      return true;
   }
   
   public boolean addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
      getPropertyChangeSupport().addPropertyChangeListener(propertyName, listener);
      return true;
   }
   
	public boolean removePropertyChangeListener(PropertyChangeListener listener) {
		if (listeners != null) {
			listeners.removePropertyChangeListener(listener);
		}
		return true;
	}

	public boolean removePropertyChangeListener(String property,
			PropertyChangeListener listener) {
		if (listeners != null) {
			listeners.removePropertyChangeListener(property, listener);
		}
		return true;
	}

   //==========================================================================


   public void removeYou()
   {

      getPropertyChangeSupport().firePropertyChange("REMOVE_YOU", this, null);
   }


   private class DirChangeListener extends Thread
   {

      private InputStreamReader reader;

      @Override
      public void run()
      {
         this.setName("DirChangeListener");
         while (true)
         {
            WatchKey watchKey = null;
            try
            {
               watchKey = watcher.take();
               for (WatchEvent event : watchKey.pollEvents())
               {
                  WatchEvent.Kind kind = event.kind();

                  if (kind == OVERFLOW) 
                  {
                     continue;
                  }
                  else if (kind == ENTRY_CREATE)
                  {
                     // if its a new json file, read it
                     WatchEvent ev = (WatchEvent)event;

                     Path filepath = ev.context();

                     String filename = modelDir + "/" + filepath.toString();

                     if (filename.endsWith(JSONCHGS) && ! filename.endsWith(userName + JSONCHGS))
                     {
                        System.out.println("New (version of) file " + filename + " detected");

                        readChangesTask(filename);
                     }
                  }
                  else if (kind == ENTRY_MODIFY)
                  {
                     // do I have a buf for this one, then read
                     WatchEvent ev = (WatchEvent)event;

                     Path filepath = ev.context();

                     String filename = modelDir + "/" + filepath.toString();

                     readChangesTask(filename);

                  }
                  else if (kind == ENTRY_DELETE)
                  {
                     continue;
                  }                                     
               }
            }
            catch (Exception e)
            {
               e.printStackTrace();
            }
            finally {
               if (watchKey != null)
               {
                  boolean reset = watchKey.reset();
               }
            }
         }
      }
   }

   @SuppressWarnings("resource")
   public ModelSpace open(String path)
   {
      // get or create model space directory
      modelDir = new File(path);

      if (! modelDir.exists())
      {
         modelDir.mkdirs();
      }

      if (! modelDir.isDirectory())
      {
         throw new RuntimeException("path " + path + " does not refer to a directory.");
      }

      // watch directory for new Files
      try
      {
         watcher = FileSystems.getDefault().newWatchService();

         Path dirPath = Paths.get(path);
         dirPath.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);

         new DirChangeListener().start();
      }
      catch (IOException e)
      {
         e.printStackTrace();
      }

      // load all json files, store read positions
      for (File f : modelDir.listFiles())
      {
         if (f.getName().endsWith(ModelSpace.JSONCHGS))
         {
            readChanges(path + "/" + f.getName());
         }
      }

      return this;
   }

   public void readChangesTask(final String filename)
   {
      if (appType == ApplicationType.JavaFX)
      {
         Platform.runLater(new Runnable()
         {
            @Override
            public void run()
            {
               readChanges(filename);
            }
         });
      }
      else
      {
         try
         {
            changeQueue.put(filename);
         }
         catch (InterruptedException e)
         {
            e.printStackTrace();
         }
      }
   }

   public void readChanges(String fileName)
   {
      try 
      {
         File file = new File(fileName);
         
         String canonicalPath = file.getCanonicalPath();
      
         if (file.exists())
         {
            long filePos = 0; 
            
            Long oldFilePos = fileReaders.get(canonicalPath);
            
            if (oldFilePos != null)
            {
               filePos = oldFilePos;
            }

            long fileLength = file.length();
            
            if (fileLength == filePos)
            {
               return;
            }
            else if (fileLength < filePos)
            {
               oldFilePos = 0L;
               filePos = 0;
            }
            
            FileChannel fileChannel = FileChannel.open(Paths.get(canonicalPath), StandardOpenOption.READ);
            
            fileChannel.position(filePos);
            
            
            int bytesToRead = (int) (fileLength - filePos);
            
            ByteBuffer byteBuf = ByteBuffer.allocate(bytesToRead);

            int read = -1;
            do {
               read = fileChannel.read(byteBuf);
            }
            while (read >= 0 && fileChannel.position() < fileLength);
            
            long newFilePos = fileChannel.position();
            
            fileReaders.put(canonicalPath, newFilePos);
            
            fileChannel.close();
            
            // byteBuf should contain new content, turn into String
            String fileContent = new String(byteBuf.array());
            String[] lines = fileContent.split("\n");
            
//            System.out.println("Read from " + canonicalPath + "\n" + fileContent);
            
            for (String line : lines)
            {
               handleChange(line);
            }
         }
      }
      catch (Exception e)
      {
         e.printStackTrace();
      }
   }

   private void handleChange(String line)
   {
      // line is a json string describing a change
      JsonObject jsonObject = new JsonObject().withValue(line);

      this.isApplyingChangeMsg = true;
      try 
      {
         ChangeEvent change = new ChangeEvent(jsonObject);
         historyPos = history.addChange(change);

         if (historyPos < 0)
         {
            // change already known, ignore
            return;
         }

         // try to apply change
         applyChange(change);
      }
      catch (Exception e)
      {
         e.printStackTrace();
      }
      finally
      {
         this.isApplyingChangeMsg = false;
      }
   }

   private void applyChange(ChangeEvent change)
   {
      Object object = idMap.getObject(change.getObjectId());

      String objectType = change.getObjectType();

      SendableEntityCreator creator = idMap.getCreator(objectType, false, null);

      if (object == null)
      {
         // new object, create it
         object = creator.getSendableInstance(false);
         idMap.put(change.getObjectId(), object);
      }

      if (ChangeEvent.PLAIN.equals(change.getPropertyKind()))
      {
         // simple attribute just do assignment
         creator.setValue(object, change.getProperty(), change.getNewValue(), null);
      }
      else if (ChangeEvent.TO_ONE.equals(change.getPropertyKind()))
      {
         String newValueId = change.getNewValue();

         if (newValueId == null)
         {
            // set pointer to null
            creator.setValue(object, change.getProperty(), null, null);
         }
         else
         {
            // provide target object
            Object targetObject = idMap.getObject(newValueId);

            if (targetObject == null)
            {
               // not yet known target, build it. 
               SendableEntityCreator targetCreator = idMap.getCreator(change.getValueType(), false, null);
               targetObject = targetCreator.getSendableInstance(false);
               idMap.put(newValueId, targetObject);
            }

            // assign value
            creator.setValue(object, change.getProperty(), targetObject, null);
         }
      }
      else // toMany
      {
         String targetId = change.getNewValue();

         if (targetId == null)
         {
            // remove the object from the to_many attribute
            targetId = change.getOldValue();

            Object targetObject = idMap.getObject(targetId);

            if (targetObject != null)
            {
               creator.setValue(object, change.getProperty(), targetObject, SendableEntityCreator.REMOVE);
            }
         }
         else
         {
            // insertion
            Object targetObject = idMap.getObject(targetId);

            if (targetObject == null)
            {
               // create unknown target
               SendableEntityCreator targetCreator = idMap.getCreator(change.getValueType(), false, null);
               targetObject = targetCreator.getSendableInstance(false);
               idMap.put(targetId, targetObject);
            }

            // assign value
            creator.setValue(object, change.getProperty(), targetObject, null);

            // try to adjust position
            tryToAdjustPosition(object, change.getProperty(), targetObject, creator);
         }
      }

      writeChange(change);

   }


   private void writeChange(ChangeEvent change)
   {
      if (isApplyingChangeMsg)
      {
         return;
      }

      try
      {
         if (logFile == null)
         {
            logFile = new File(modelDir.getCanonicalPath() + "/" + userName + ModelSpace.JSONCHGS);

            logFile.createNewFile();
         }

         if (logFileWriter == null)
         {
            logFileWriter = new FileWriter(logFile, true);
         }

         JsonObject jsonObject = change.toJson();
         logFileWriter.write(jsonObject.toString() + "\n");
         logFileWriter.flush();

         if (logPath == null)
         {
            logPath = Paths.get(modelDir.getCanonicalPath() + "/" + userName + ModelSpace.JSONCHGS);
         }

         Files.setLastModifiedTime(logPath, FileTime.fromMillis(System.currentTimeMillis()));
      }
      catch (IOException e)
      {
         e.printStackTrace();
      }

   }


   private void tryToAdjustPosition(Object object, String property, Object targetObject, SendableEntityCreator creator)
   {
      Object value = creator.getValue(object, property);

      if (value != null && value instanceof AbstractList)
      {
         AbstractList valueList = (AbstractList) value;

         int indexOf = valueList.indexOf(targetObject);

         if (indexOf != historyPos)
         {
            valueList.remove(indexOf);
            valueList.add(historyPos, targetObject);
         }        
      }
   }


   //==============================================================================
   public long getNewHistoryIdNumber()
   {
      long result = System.currentTimeMillis();

      if (result <= lastChangeId)
      {
         result = lastChangeId + 1;
      }

      lastChangeId = result;

      return result;
   }

   @Override
	public boolean update(Object event) {
      if (isApplyingChangeMsg)
      {
         // ignore
         return true;
      }
      
      SimpleEvent simpleEvent = (SimpleEvent) event;
      
      if (simpleEvent.getEntity() == null) 
      {
         // looks like a bug in IDMap. It fires an empty property change within 
         // Filter.isPropertyRegard 
         return false;
      }

      JsonObject jsonObject = (JsonObject) simpleEvent.getEntity();

      // {"id":"testerProxy",
      //  "class":"org.sdmlib.replication.SeppelSpaceProxy",
      //  "upd":{"scopes":{"class":"org.sdmlib.replication.SeppelScope",
      //                   "id":"tester.S1",
      //                   "prop":{"scopeName":"commands",
      //                           "spaces":[{"id":"testerProxy"}]}}}}

      String opCode = SendableEntityCreator.UPDATE;

      Object attributes = jsonObject.get(SendableEntityCreator.UPDATE);

      if (attributes == null)
      {
         attributes = jsonObject.get(SendableEntityCreator.REMOVE);
         opCode = SendableEntityCreator.REMOVE;

         if (attributes == null)
         {
            attributes = jsonObject.get("prop");
            opCode = SendableEntityCreator.UPDATE;
         }
      }

      JsonObject valueJsonObject = null;
      JsonObject attributesJson = null;
      String prop = null;

      if (attributes != null)
      {
         attributesJson = (JsonObject) attributes;

         Iterator iter = attributesJson.keyIterator();
         while ( iter.hasNext())
         {
            prop = iter.next();

            ChangeEvent change = new ChangeEvent()
            .withSessionId(userName)
            .withChangeNo("" + getNewHistoryIdNumber())
            .withObjectId(jsonObject.getString(IdMap.ID))
            .withObjectType(jsonObject.getString(IdMap.CLASS))
            .withProperty(prop);

            Object attrValue = attributesJson.get(prop);

            if (attrValue instanceof JsonObject)
            {
               JsonArray valueJsonArray = new JsonArray();
               valueJsonArray.add(attrValue);
               attrValue = valueJsonArray;
            }

            if (attrValue instanceof JsonArray)
            {
               JsonArray valueJsonArray = (JsonArray) attrValue;

               for (Object arrayElem : valueJsonArray)
               {
                  valueJsonObject = (JsonObject) arrayElem;

                  String valueObjectId = (String) valueJsonObject.get(IdMap.ID);

                  String valueObjectType = (String) valueJsonObject.get(IdMap.CLASS);

                  Object valueObject = idMap.getObject(valueObjectId);

                  if (valueObjectType == null)
                  {
                     // get object and ask it
                     valueObjectType = valueObject.getClass().getName();
                  }

                  change.withValueType(valueObjectType);

                  // toOne or toMany
                  change.withPropertyKind(ChangeEvent.TO_ONE);

                  Object targetObject = idMap.getObject(change.getObjectId());

                  SendableEntityCreator creator = idMap.getCreatorClass(targetObject);

                  Object value = creator.getValue(targetObject, change.getProperty());

                  if (value != null && value instanceof Collection)
                  {
                     change.setPropertyKind(ChangeEvent.TO_MANY);
                  }

                  // newValue or oldValue?
                  if (opCode.equals(SendableEntityCreator.REMOVE))
                  {
                     change.withOldValue(valueObjectId);
                  }
                  else
                  {
                     change.withNewValue(valueObjectId);
                  }

                  // store it
                  getHistory().addChange(change);
                  writeChange(change);

                  // does the value have properties?
                  if (valueJsonObject.get("prop") != null)
                  {
                     // call recursive
//                     this.update(typ, valueJsonObject, valueObject, prop, null, null);
                	  simpleEvent.with(valueJsonObject);
                	  this.update(simpleEvent);
                  }
               }
            }
            else
            {
            	PropertyChangeEvent evt = (PropertyChangeEvent) event;
               String oldValueString = "" + evt.getOldValue();
               if (evt.getOldValue() == null)
               {
                  oldValueString = null;
               }

               // plain attribute
               change.withPropertyKind(ChangeEvent.PLAIN)
               .withNewValue("" + attrValue)
               .withOldValue(oldValueString);

               getHistory().addChange(change);
               writeChange(change);
            }
         }
      }


      return true;
   }

   public boolean firePropertyChange(String propertyName, Object oldValue, Object newValue)
   {
      if (listeners != null) {
   		listeners.firePropertyChange(propertyName, oldValue, newValue);
   		return true;
   	}
   	return false;
   }
   }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy