<file_sep>package org.fulib.yaml;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * A container for arbitrary attributes.
 * Simple attributes can be set with {@link #put(String, Object)} and get with {@link #get(String)}.
 * Collection attributes can be created with {@link #with(String, Object)}.
 */
public class YamlObject
{
   // =============== Constants ===============

   public static final String ID = ".id";
   public static final String TYPE = "type";

   // =============== Fields ===============

   private LinkedHashMap<String, Object> map = new LinkedHashMap<>();

   // =============== Constructors ===============

   public YamlObject()
   {
      // empty
   }

   /**
    * @param id
    *    the {@link #setId(String) id}
    *
    * @since 1.2
    */
   public YamlObject(String id)
   {
      this.setId(id);
   }

   /**
    * @param id
    *    the {@link #setId(String) id}
    * @param type
    *    the {@link #setType(String) type}
    */
   public YamlObject(String id, String type)
   {
      this(id);
      this.setType(type);
   }

   // =============== Properties ===============

   /**
    * @return the map of property names to property values
    *
    * @deprecated since 1.2; use {@link #getProperties()} instead
    */
   @Deprecated
   public LinkedHashMap<String, Object> getMap()
   {
      return this.map;
   }

   /**
    * @return the map of property names to property values
    *
    * @since 1.2
    */
   public Map<String, Object> getProperties()
   {
      return this.map;
   }

   /**
    * Convenience getter for {@link #get(String) get}({@link #ID}).
    * In particular, roughly implemented as
    *
    * <pre><code>
    *    final Object id = this.get(ID);
    *    return id != null ? id.toString() : null;
    * </code></pre>
    *
    * @return the ID
    *
    * @since 1.2
    */
   public String getId()
   {
      final Object id = this.map.get(ID);
      return id != null ? id.toString() : null;
   }

   /**
    * Convenience setter for {@link #put(String, Object) put}({@link #ID}, {@code id}).
    *
    * @param id
    *    the ID
    *
    * @since 1.2
    */
   public void setId(String id)
   {
      this.map.put(ID, id);
   }

   /**
    * Convenience getter for {@link #get(String) get}({@link #TYPE}).
    * In particular, roughly implemented as
    *
    * <pre><code>
    *    final Object type = this.get(TYPE);
    *    return type != null ? type.toString() : null;
    * </code></pre>
    *
    * @return the type
    *
    * @since 1.2
    */
   public String getType()
   {
      final Object type = this.map.get(TYPE);
      return type != null ? type.toString() : null;
   }

   /**
    * Convenience setter for {@link #put(String, Object) put}({@link #TYPE}, {@code type}).
    *
    * @param type
    *    the type
    *
    * @since 1.2
    */
   public void setType(String type)
   {
      this.map.put(TYPE, type);
   }

   // =============== Methods ===============

   /**
    * @param property
    *    the property name
    *
    * @return the attribute value for the given property name, or {@code null} if not specified
    */
   public Object get(String property)
   {
      return this.map.get(property);
   }

   /**
    * Sets the attribute value for the given property name.
    *
    * @param property
    *    the property name
    * @param value
    *    the attribute value
    *
    * @return this instance, to allow method chaining
    */
   public YamlObject put(String property, Object value)
   {
      this.map.put(property, value);
      return this;
   }

   /**
    * Adds the item to the collection attribute with the given property name.
    * In particular, the implementation is as follows:
    *
    * <ol>
    *   <li>If the property is not set, set the item as the attribute value as if
    *   {@link #put(String, Object)}({@code property}, {@code item}) was called</li>
    *   <li>If the property is already set and the value is a {@link List}, add the {@code item} to that list</li>
    *   <li>If the property is already set and the value is not a {@link List},
    *   create a new list with the old value and the {@code item} and set that as the new value</li>
    * </ol>
    *
    * @param property
    *    the property name
    * @param item
    *    the item to add or set
    *
    * @return this instance, to allow method chaining
    */
   public YamlObject with(String property, Object item)
   {
      this.map.compute(property, (k, oldValue) -> {
         if (oldValue == null) // not yet present
         {
            final List<Object> list = new ArrayList<>();
            list.add(item);
            return list;
         }

         final List<Object> list;
         if (oldValue instanceof List) // old value was a list
         {
            list = (List<Object>) oldValue;
         }
         else // old value was an object
         {
            list = new ArrayList<>();
            list.add(oldValue);
         }
         list.add(item);
         return list;
      });
      return this;
   }

   @Override
   public String toString()
   {
      final String id = this.getId();
      return id != null ? id : super.toString();
   }
}
<file_sep>plugins {
   id 'java'
   id 'maven-publish'
   // https://plugins.gradle.org/plugin/com.palantir.revapi
   id 'com.palantir.revapi' version '1.4.4'
   // https://plugins.gradle.org/plugin/com.bmuschko.nexus
   id 'com.bmuschko.nexus' version '2.3.1'
   // https://plugins.gradle.org/plugin/io.codearte.nexus-staging
   id 'io.codearte.nexus-staging' version '0.22.0'
}

group = 'org.fulib'
version = 'git describe --tags'.execute().text[1..-2] // strip v and \n
description = 'Yaml serialization for fulib object models. Used for storage and communication.'

modifyPom {
   project {
      name = project.name
      description = project.description
      url = 'https://github.com/fujaba/fulibYaml'
      inceptionYear = '2018'

      scm {
         url = 'https://github.com/fujaba/fulibYaml'
      }

      licenses {
         license {
            name = 'MIT License'
            url = 'http://www.opensource.org/licenses/mit-license.php'
         }
      }

      developers {
         developer {
            id = 'zuendorf'
            name = '<NAME>'
         }
         developer {
            id = 'digitalhoax'
            name = '<NAME>'
         }
         developer {
            id = 'eicke123'
            name = '<NAME>'
         }
         developer {
            id = 'clashsoft'
            name = '<NAME>'
         }
      }
   }
}

sourceCompatibility = 1.8

// --------------- Dependencies ---------------

repositories {
   mavenLocal()
   mavenCentral()
}

dependencies {
   // https://mvnrepository.com/artifact/org.hamcrest/hamcrest-library
   testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '2.2'

   // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
   testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.1'

   // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine
   testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.1'
}

// --------------- Misc. Settings ---------------

test {
   useJUnitPlatform()
}

revapi {
   oldVersion = '1.4.0'
}
<file_sep>package org.fulib.yaml.testmodel.subpackage;

public class Room
{
   private String id;

   public String getId()
   {
      return this.id;
   }

   public Room setId(String id)
   {
      this.id = id;
      return this;
   }
}
<file_sep>package org.fulib.yaml;

import java.beans.PropertyChangeEvent;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

public class EventYamler
{
   public static final String TIME = "time";
   public static final String SOURCE = "source";
   public static final String SOURCE_TYPE = "sourceType";
   public static final String PROPERTY = "property";
   public static final String OLD_VALUE = "oldValue";
   public static final String OLD_VALUE_TYPE = OLD_VALUE + "Type";
   public static final String NEW_VALUE = "newValue";
   public static final String NEW_VALUE_TYPE = NEW_VALUE + "Type";
   public static final String HISTORY_KEY = "historyKey";

   private YamlIdMap yamlIdMap;

   public EventYamler(String packageName)
   {
      this(new YamlIdMap(packageName));
   }

   /**
    * @since 1.2
    */
   public EventYamler(YamlIdMap idMap)
   {
      this.yamlIdMap = idMap;
   }

   /**
    * @since 1.2
    */
   public YamlIdMap getYamlIdMap()
   {
      return this.yamlIdMap;
   }

   public EventYamler setYamlIdMap(YamlIdMap yamlIdMap)
   {
      this.yamlIdMap = yamlIdMap;

      return this;
   }

   public String encode(PropertyChangeEvent e)
   {
      Object source = e.getSource();
      StringBuilder buf = new StringBuilder("- ");

      long timeMillis = System.currentTimeMillis();
      Date date = new Date(timeMillis);
      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
      String timeString = dateFormat.format(date);
      buf.append(TIME + ": ").append(timeString).append("\n");

      String sourceKey = this.yamlIdMap.putObject(source);
      buf.append("  " + SOURCE + ": ").append(sourceKey).append("\n");

      String className = source.getClass().getSimpleName();
      buf.append("  " + SOURCE_TYPE + ": ").append(className).append("\n");

      String prop = e.getPropertyName();
      buf.append("  " + PROPERTY + ": ").append(prop).append("\n");

      String historyKey = sourceKey + "/" + prop;

      Object oldValue = e.getOldValue();
      if (oldValue != null)
      {
         Class<?> valueClass = oldValue.getClass();

         if (valueClass == String.class)
         {
            String encapsulted = YamlGenerator.encapsulate((String) oldValue);
            buf.append("  " + OLD_VALUE + ": ").append(encapsulted).append("\n");
         }
         else if (valueClass.getName().startsWith("java.lang."))
         {
            buf.append("  " + OLD_VALUE + ": ").append(oldValue).append("\n");
         }
         else
         {
            String valueKey = this.yamlIdMap.putObject(oldValue);
            buf.append("  " + OLD_VALUE + ": ").append(valueKey).append("\n");

            historyKey += "/" + valueKey;

            className = oldValue.getClass().getSimpleName();
            buf.append("  " + OLD_VALUE_TYPE + ": ").append(className).append("\n");
         }
      }

      Object newValue = e.getNewValue();
      if (newValue != null)
      {
         Class<?> valueClass = newValue.getClass();

         if (valueClass == String.class)
         {
            String encapsulted = YamlGenerator.encapsulate((String) newValue);
            buf.append("  " + NEW_VALUE + ": ").append(encapsulted).append("\n");
         }
         else if (valueClass.getName().startsWith("java.lang."))
         {
            buf.append("  " + NEW_VALUE + ": ").append(newValue).append("\n");
         }
         else
         {
            String valueKey = this.yamlIdMap.putObject(newValue);
            buf.append("  " + NEW_VALUE + ": ").append(valueKey).append("\n");

            Reflector reflector = this.yamlIdMap.getReflector(className);
            Object attrValue = reflector.getValue(source, prop);
            if (attrValue != null && Collection.class.isAssignableFrom(attrValue.getClass()))
            {
               historyKey += "/" + valueKey;
            }

            className = newValue.getClass().getSimpleName();
            buf.append("  " + NEW_VALUE_TYPE + ": ").append(className).append("\n");
         }
      }

      buf.append("  " + HISTORY_KEY + ": ").append(historyKey).append("\n");
      buf.append("\n");
      return buf.toString();
   }

   public Object decode(Object rootObject, String content)
   {
      Yamler yamler = new Yamler();
      ArrayList<LinkedHashMap<String, String>> list = yamler.decodeList(content);

      String firstKey = null;
      for (LinkedHashMap<String, String> map : list)
      {
         // execute change
         String sourceKey = map.get(SOURCE);

         if (firstKey == null)
         {
            firstKey = sourceKey;
            Object oldObject = this.yamlIdMap.getObject(firstKey);
            if (oldObject == null)
            {
               this.yamlIdMap.putNameObject(firstKey, rootObject);
            }
         }

         Object sourceObject = this.yamlIdMap.getObject(sourceKey);
         String className = map.get(SOURCE_TYPE);
         Reflector reflector = this.yamlIdMap.getReflector(className);

         if (sourceObject == null)
         {
            sourceObject = reflector.newInstance();
            this.yamlIdMap.putNameObject(sourceKey, sourceObject);
         }

         String property = map.get(PROPERTY);
         String newValue = map.get(NEW_VALUE);
         String newValueType = map.get(NEW_VALUE_TYPE);

         if (newValueType == null)
         {
            reflector.setValue(sourceObject, property, newValue);
         }
         else
         {
            Object newValueObject = this.yamlIdMap.getObject(newValue);
            if (newValueObject == null)
            {
               Reflector newValueReflector = this.yamlIdMap.getReflector(newValueType);
               newValueObject = newValueReflector.newInstance();
               this.yamlIdMap.putNameObject(newValue, newValueObject);
            }

            reflector.setValue(sourceObject, property, newValueObject);
         }
      }

      return this.yamlIdMap.getObject(firstKey);
   }
}
<file_sep># fulibYaml v1.0.0

# fulibYaml v1.0.1

# fulibYaml v1.0.2

# fulibYaml v1.0.3

# fulibYaml v1.1.0

* Bumped version number.

# fulibYaml v1.2.0

## New Features

+ Added the `ReflectorMap.canReflect` and `.discoverObjects` methods.
+ Added the `Reflector.getOwnProperties` and `.getAllProperties` methods.
+ Added the `YamlGenerator` class, a more streamlined API for converting events to YAML.
+ Added the `IdMap` class.

## Bugfixes

* Fixed an exception when `YamlIdMap` discovers objects of unknown types. #15

## Improvements

* `YamlIdMap` no longer appends auto-incremented numbers to IDs if not necessary. #16

## General

* Transitioned many APIs from concrete implementation types like `ArrayList` or `LinkedHashMap` to their respective 
  interfaces like `List` or `Map`.
  > In places where this could not be done in-place, new APIs were added and the old ones deprecated.
  > See the respective Javadocs for migration info.
* Deprecated some misplaced or accidentally public APIs.
  > Check for deprecation warnings and see the respective Javadocs for migration info.
* General code cleanup and minor optimizations.

# fulibYaml v1.2.1

## Bugfixes

* The `ReflectorMap.discoverObjects` methods now use breadth-first search.
  > This restores the previous order in which `YamlIdMap`s were serialized.

# fulibYaml v1.3.0

## New Features

+ Added support for enums in property values. #12
+ Added support for `byte`, `short`, `char` and corresponding wrapper types in property values.
+ Added the `YamlGenerator.quote` methods.

## Improvements

* The `Reflector.getValue` and `.setValue` methods no longer silently ignore exceptions within the invoked methods.
* The `Reflector.getValue` and `.setValue` methods now cache target methods to improve performance.
* Changed the algorithm used by `ReflectorMap.setValue`.
  > It now tries to invoke all *public* `set` and `with` methods in the order in which they are in the bytecode.
  > Coercions (varargs, string to primitives) are performed as needed.
  > Note that it does not perform full overload resolution, so results may differ from calling the appropriate setter normally.
* The `Reflector.setValue` method now supports the `with` overloads generated by Fulib v1.2.
* The `YamlIdMap.encode` method can now handle collections of values.

## Bugfixes

* Fixed the `YamlGenerator.encapsulate(String, Appendable)` method appending the value twice.

# fulibYaml v1.4.0

## New Features

+ Added the `Reflector.removeValue` method for removing links and multi-attribute values using `without` methods. #18 #20
+ `YamlIdMap` now also serializes properties from superclasses and interfaces. #24

## Bugfixes

* Fixed an exception when serializing an object with an empty ID. #21 #22
* `Reflector.getOwnProperties` no longer returns properties from superclasses and interfaces. #23

# fulibYaml v1.5.0

## New Features

+ Added the `Yaml` class with a new simple API. #25

## Improvements

* `ReflectorMap` can now resolve fully qualified class names. #26
* `YamlIdMap` now uses fully qualified class names for encoding. #26
<file_sep>package org.fulib.yaml.testmodel;

public class Person
{
   private String name;
   private Day favoriteDay;

   public String getName()
   {
      return this.name;
   }

   public void setName(String name)
   {
      this.name = name;
   }

   public Day getFavoriteDay()
   {
      return this.favoriteDay;
   }

   public void setFavoriteDay(Day favorityDay)
   {
      this.favoriteDay = favorityDay;
   }
}
<file_sep>package org.fulib.yaml.testmodel;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Offer  
{

   public static final String PROPERTY_id = "id";

   private String id;

   public String getId()
   {
      return id;
   }

   public Offer setId(String value)
   {
      if (value == null ? this.id != null : ! value.equals(this.id))
      {
         String oldValue = this.id;
         this.id = value;
         firePropertyChange("id", oldValue, value);
      }
      return this;
   }

   public static final String PROPERTY_price = "price";

   private double price;

   public double getPrice()
   {
      return price;
   }

   public Offer setPrice(double value)
   {
      if (value != this.price)
      {
         double oldValue = this.price;
         this.price = value;
         firePropertyChange("price", oldValue, value);
      }
      return this;
   }

   public static final String PROPERTY_startTime = "startTime";

   private String startTime;

   public String getStartTime()
   {
      return startTime;
   }

   public Offer setStartTime(String value)
   {
      if (value == null ? this.startTime != null : ! value.equals(this.startTime))
      {
         String oldValue = this.startTime;
         this.startTime = value;
         firePropertyChange("startTime", oldValue, value);
      }
      return this;
   }

   public static final String PROPERTY_endTime = "endTime";

   private String endTime;

   public String getEndTime()
   {
      return endTime;
   }

   public Offer setEndTime(String value)
   {
      if (value == null ? this.endTime != null : ! value.equals(this.endTime))
      {
         String oldValue = this.endTime;
         this.endTime = value;
         firePropertyChange("endTime", oldValue, value);
      }
      return this;
   }

   protected PropertyChangeSupport listeners = null;

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

   public boolean addPropertyChangeListener(PropertyChangeListener listener)
   {
      if (listeners == null)
      {
         listeners = new PropertyChangeSupport(this);
      }
      listeners.addPropertyChangeListener(listener);
      return true;
   }

   public boolean addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
   {
      if (listeners == null)
      {
         listeners = new PropertyChangeSupport(this);
      }
      listeners.addPropertyChangeListener(propertyName, listener);
      return true;
   }

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

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

   @Override
   public String toString()
   {
      StringBuilder result = new StringBuilder();

      result.append(" ").append(this.getId());
      result.append(" ").append(this.getStartTime());
      result.append(" ").append(this.getEndTime());


      return result.substring(1);
   }

   public static final String PROPERTY_product = "product";

   private Product product = null;

   public Product getProduct()
   {
      return this.product;
   }

   public Offer setProduct(Product value)
   {
      if (this.product != value)
      {
         Product oldValue = this.product;
         if (this.product != null)
         {
            this.product = null;
            oldValue.withoutOffers(this);
         }
         this.product = value;
         if (value != null)
         {
            value.withOffers(this);
         }
         firePropertyChange("product", oldValue, value);
      }
      return this;
   }

   public void removeYou()
   {
      this.setProduct(null);

      this.withoutOrders(this.getOrders().clone());


   }

   public static final java.util.ArrayList<OrderPosition> EMPTY_orders = new java.util.ArrayList<OrderPosition>()
   { @Override public boolean add(OrderPosition value){ throw new UnsupportedOperationException("No direct add! Use xy.withOrders(obj)"); }};

   public static final String PROPERTY_orders = "orders";

   private java.util.ArrayList<OrderPosition> orders = null;

   public java.util.ArrayList<OrderPosition> getOrders()
   {
      if (this.orders == null)
      {
         return EMPTY_orders;
      }

      return this.orders;
   }

   public Offer withOrders(Object... value)
   {
      if(value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withOrders(i);
            }
         }
         else if (item instanceof OrderPosition)
         {
            if (this.orders == null)
            {
               this.orders = new java.util.ArrayList<OrderPosition>();
            }
            if ( ! this.orders.contains(item))
            {
               this.orders.add((OrderPosition)item);
               ((OrderPosition)item).setOffer(this);
               firePropertyChange("orders", null, item);
            }
         }
         else throw new IllegalArgumentException();
      }
      return this;
   }

   public Offer withoutOrders(Object... value)
   {
      if (this.orders == null || value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withoutOrders(i);
            }
         }
         else if (item instanceof OrderPosition)
         {
            if (this.orders.contains(item))
            {
               this.orders.remove((OrderPosition)item);
               ((OrderPosition)item).setOffer(null);
               firePropertyChange("orders", item, null);
            }
         }
      }
      return this;
   }

}
<file_sep>package org.fulib.yaml;

import java.util.Arrays;
import java.util.Map;

/**
 * Simple yaml encoding
 *
 * @since 1.5
 */
public class Yaml
{
   private Yaml()
   {
      // no instances
   }

   public static String encode(Object... objects)
   {
      final String[] packageNames = Arrays
         .stream(objects)
         .map(Object::getClass)
         .map(Class::getPackage)
         .map(Package::getName)
         .toArray(String[]::new);
      return new YamlIdMap(packageNames).encode(objects);
   }

   public static Map<String, Object> decode(String yaml)
   {
      final YamlIdMap idMap = new YamlIdMap();
      idMap.decode(yaml);
      return idMap.getObjIdMap();
   }
}
<file_sep>package org.fulib.yaml;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

public class EventFiler
{
   // =============== Fields ===============

   private final EventSource eventSource;

   private String historyFileName = null;

   // =============== Constructors ===============

   public EventFiler(EventSource eventSource)
   {
      this.eventSource = eventSource;
   }

   // =============== Properties ===============

   /**
    * @since 1.2
    */
   public EventSource getEventSource()
   {
      return this.eventSource;
   }

   /**
    * @since 1.2
    */
   public String getHistoryFileName()
   {
      return this.historyFileName;
   }

   public EventFiler setHistoryFileName(String historyFileName)
   {
      this.historyFileName = historyFileName;
      return this;
   }

   // =============== Methods ===============

   public String loadHistory()
   {
      final Path historyFile = Paths.get(this.historyFileName);
      if (Files.exists(historyFile))
      {
         return null;
      }
      try
      {
         return new String(Files.readAllBytes(historyFile));
      }
      catch (IOException e)
      {
         return null;
      }
   }

   public boolean storeHistory()
   {
      final Path historyFile = Paths.get(this.historyFileName);

      createDirs(historyFile);

      try (final Writer writer = Files.newBufferedWriter(historyFile, StandardOpenOption.CREATE))
      {
         this.eventSource.encodeYaml(writer);
      }
      catch (IOException e)
      {
         Logger.getGlobal().log(Level.SEVERE, "could not write to historyFile " + historyFile, e);
         return false;
      }

      return true;
   }

   public EventFiler startEventLogging()
   {
      this.eventSource.addEventListener(this::storeEvent);

      return this;
   }

   /**
    * Appends the event to the history file.
    *
    * @param event
    *    the event
    *
    * @deprecated since 1.2; use {@link #storeEvent(Map)}
    */
   @Deprecated
   public void storeEvent(LinkedHashMap<String, String> event)
   {
      this.storeEvent((Map<String, String>) event);
   }

   /**
    * Appends the event to the history file.
    *
    * @param event
    *    the event
    *
    * @since 1.2
    */
   public void storeEvent(Map<String, String> event)
   {
      final Path historyFile = Paths.get(this.historyFileName);

      createDirs(historyFile);

      try (final Writer writer = Files.newBufferedWriter(historyFile, StandardOpenOption.CREATE, StandardOpenOption.APPEND))
      {
         YamlGenerator.serialize(event, writer);
      }
      catch (IOException e)
      {
         Logger.getGlobal().log(Level.SEVERE, "could not write to historyFile " + historyFile, e);
      }
   }

   private static void createDirs(Path historyFile)
   {
      try
      {
         Files.createDirectories(historyFile.getParent());
      }
      catch (IOException e)
      {
         Logger.getGlobal().log(Level.SEVERE, "could create directories for historyFile " + historyFile, e);
      }
   }
}
<file_sep>package org.fulib.yaml;

import java.io.IOException;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * This class provides static utility methods for generating Yaml strings.
 *
 * @since 1.2
 */
public class YamlGenerator
{
   // =============== Constants ===============

   private static final Pattern SIMPLE_VALUE_PATTERN = Pattern.compile("[a-zA-Z0-9_.]+");

   // =============== Static Methods ===============

   /**
    * Encodes the events as a list of YAML objects.
    *
    * @param events
    *    the events
    *
    * @return the encoded YAML object list
    */
   public static String serialize(Iterable<? extends Map<String, String>> events)
   {
      final StringBuilder builder = new StringBuilder();
      try
      {
         serialize(events, builder);
      }
      catch (IOException e)
      {
         // cannot happen
      }
      return builder.toString();
   }

   /**
    * Encodes the events as a list of YAML objects into the writer.
    *
    * @param events
    *    the events
    * @param writer
    *    the writer
    *
    * @throws IOException
    *    when appending to the writer produces an error
    */
   public static void serialize(Iterable<? extends Map<String, String>> events, Appendable writer) throws IOException
   {
      for (final Map<String, String> event : events)
      {
         serialize(event, writer);
      }
   }

   /**
    * Encodes the event as a YAML object.
    *
    * @param event
    *    the event
    *
    * @return the encoded YAML object
    */
   public static String serialize(Map<String, String> event)
   {
      final StringBuilder writer = new StringBuilder();
      try
      {
         serialize(event, writer);
      }
      catch (IOException e)
      {
         // cannot happen
      }
      return writer.toString();
   }

   /**
    * Encodes the event as a YAML object into the writer.
    *
    * @param event
    *    the event
    * @param writer
    *    the writer
    *
    * @throws IOException
    *    when appending to the writer produces an error
    */
   public static void serialize(Map<String, String> event, Appendable writer) throws IOException
   {
      String prefix = "- ";
      for (Map.Entry<String, String> keyValuePair : event.entrySet())
      {
         writer.append(prefix);
         writer.append(keyValuePair.getKey());
         writer.append(": ");
         encapsulate(keyValuePair.getValue(), writer);
         writer.append('\n');
         prefix = "  ";
      }
      writer.append('\n');
   }

   /**
    * Encapsulates a YAML value by enclosing it in quotes ("), if necessary.
    *
    * @param value
    *    the YAML value to encapsulate
    *
    * @return the encapsulated YAML value
    */
   public static String encapsulate(String value)
   {
      if (SIMPLE_VALUE_PATTERN.matcher(value).matches())
      {
         return value;
      }
      return quote(value);
   }

   /**
    * Encapsulates a YAML value by enclosing it in quotes ("), if necessary, and appends the result to the writer.
    *
    * @param value
    *    the YAML value to encapsulate
    * @param writer
    *    the writer
    *
    * @throws IOException
    *    when appending to the writer produces an error
    */
   public static void encapsulate(String value, Appendable writer) throws IOException
   {
      if (SIMPLE_VALUE_PATTERN.matcher(value).matches())
      {
         writer.append(value);
         return;
      }
      quote(value, writer);
   }

   /**
    * Encapsulates a YAML value by enclosing it in quotes (").
    *
    * @param value
    *    the YAML value to quote
    *
    * @since 1.3
    */
   public static String quote(String value)
   {
      // replace " with \"
      return "\"" + value.replace("\"", "\\\"") + "\"";
   }

   /**
    * Encapsulates a YAML value by enclosing it in quotes ("), and appends the result to the writer.
    *
    * @param value
    *    the YAML value to quote
    * @param writer
    *    the writer
    *
    * @throws IOException
    *    when appending to the writer produces an error
    *
    * @since 1.3
    */
   public static void quote(String value, Appendable writer) throws IOException
   {
      writer.append('"');
      writer.append(value.replace("\"", "\\\""));
      writer.append('"');
   }
}
<file_sep>package org.fulib.yaml.testmodel;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Customer  
{

   public static final String PROPERTY_id = "id";

   private String id;

   public String getId()
   {
      return id;
   }

   public Customer setId(String value)
   {
      if (value == null ? this.id != null : ! value.equals(this.id))
      {
         String oldValue = this.id;
         this.id = value;
         firePropertyChange("id", oldValue, value);
      }
      return this;
   }

   protected PropertyChangeSupport listeners = null;

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

   public boolean addPropertyChangeListener(PropertyChangeListener listener)
   {
      if (listeners == null)
      {
         listeners = new PropertyChangeSupport(this);
      }
      listeners.addPropertyChangeListener(listener);
      return true;
   }

   public boolean addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
   {
      if (listeners == null)
      {
         listeners = new PropertyChangeSupport(this);
      }
      listeners.addPropertyChangeListener(propertyName, listener);
      return true;
   }

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

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

   @Override
   public String toString()
   {
      StringBuilder result = new StringBuilder();

      result.append(" ").append(this.getId());
      result.append(" ").append(this.getName());
      result.append(" ").append(this.getAddress());


      return result.substring(1);
   }

   public static final String PROPERTY_name = "name";

   private String name;

   public String getName()
   {
      return name;
   }

   public Customer setName(String value)
   {
      if (value == null ? this.name != null : ! value.equals(this.name))
      {
         String oldValue = this.name;
         this.name = value;
         firePropertyChange("name", oldValue, value);
      }
      return this;
   }

   public static final String PROPERTY_address = "address";

   private String address;

   public String getAddress()
   {
      return address;
   }

   public Customer setAddress(String value)
   {
      if (value == null ? this.address != null : ! value.equals(this.address))
      {
         String oldValue = this.address;
         this.address = value;
         firePropertyChange("address", oldValue, value);
      }
      return this;
   }

   public static final java.util.ArrayList<Product> EMPTY_products = new java.util.ArrayList<Product>()
   { @Override public boolean add(Product value){ throw new UnsupportedOperationException("No direct add! Use xy.withProducts(obj)"); }};

   public static final String PROPERTY_products = "products";

   private java.util.ArrayList<Product> products = null;

   public java.util.ArrayList<Product> getProducts()
   {
      if (this.products == null)
      {
         return EMPTY_products;
      }

      return this.products;
   }

   public Customer withProducts(Object... value)
   {
      if(value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withProducts(i);
            }
         }
         else if (item instanceof Product)
         {
            if (this.products == null)
            {
               this.products = new java.util.ArrayList<Product>();
            }
            if ( ! this.products.contains(item))
            {
               this.products.add((Product)item);
               ((Product)item).withCustomers(this);
               firePropertyChange("products", null, item);
            }
         }
         else throw new IllegalArgumentException();
      }
      return this;
   }

   public Customer withoutProducts(Object... value)
   {
      if (this.products == null || value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withoutProducts(i);
            }
         }
         else if (item instanceof Product)
         {
            if (this.products.contains(item))
            {
               this.products.remove((Product)item);
               ((Product)item).withoutCustomers(this);
               firePropertyChange("products", item, null);
            }
         }
      }
      return this;
   }

   public void removeYou()
   {
      this.withoutOrders(this.getOrders().clone());


      this.withoutProducts(this.getProducts().clone());


   }

   public static final java.util.ArrayList<Order> EMPTY_orders = new java.util.ArrayList<Order>()
   { @Override public boolean add(Order value){ throw new UnsupportedOperationException("No direct add! Use xy.withOrders(obj)"); }};

   public static final String PROPERTY_orders = "orders";

   private java.util.ArrayList<Order> orders = null;

   public java.util.ArrayList<Order> getOrders()
   {
      if (this.orders == null)
      {
         return EMPTY_orders;
      }

      return this.orders;
   }

   public Customer withOrders(Object... value)
   {
      if(value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withOrders(i);
            }
         }
         else if (item instanceof Order)
         {
            if (this.orders == null)
            {
               this.orders = new java.util.ArrayList<Order>();
            }
            if ( ! this.orders.contains(item))
            {
               this.orders.add((Order)item);
               ((Order)item).setCustomer(this);
               firePropertyChange("orders", null, item);
            }
         }
         else throw new IllegalArgumentException();
      }
      return this;
   }

   public Customer withoutOrders(Object... value)
   {
      if (this.orders == null || value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withoutOrders(i);
            }
         }
         else if (item instanceof Order)
         {
            if (this.orders.contains(item))
            {
               this.orders.remove((Order)item);
               ((Order)item).setCustomer(null);
               firePropertyChange("orders", item, null);
            }
         }
      }
      return this;
   }

}
<file_sep>package org.fulib.yaml;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

public class ModelListener implements PropertyChangeListener
{
   private final PropertyChangeListener elementListener;
   private final ReflectorMap creatorMap;

   private final Set<Object> supervisedObjects = new HashSet<>();

   private boolean closed = false;

   public ModelListener(Object root, PropertyChangeListener elementListener)
   {
      this.elementListener = elementListener;
      String packageName = root.getClass().getPackage().getName();
      this.creatorMap = new ReflectorMap(packageName);
      this.subscribeTo(root);
   }

   public void removeYou()
   {
      this.closed = true;

      for (Object obj : this.supervisedObjects)
      {
         this.callChangeListenerMethod(obj, "removePropertyChangeListener");
      }
   }

   private void subscribeTo(Object newObject)
   {
      if (this.supervisedObjects.contains(newObject))
      {
         return;
      }

      this.callChangeListenerMethod(newObject, "addPropertyChangeListener");
      this.supervisedObjects.add(newObject);

      this.fireInitialPropertyChanges(newObject);
   }

   private void fireInitialPropertyChanges(Object newObject)
   {
      if (!this.creatorMap.canReflect(newObject))
      {
         return; // don't know structure of newObject, probably a String
      }

      Reflector reflector = this.creatorMap.getReflector(newObject);

      for (String prop : reflector.getAllProperties())
      {
         Object propertyValue = reflector.getValue(newObject, prop);

         if (propertyValue instanceof Collection)
         {
            for (Object obj : (Collection<?>) propertyValue)
            {
               this.propertyChange(new PropertyChangeEvent(newObject, prop, null, obj));
            }
         }
         else
         {
            this.propertyChange(new PropertyChangeEvent(newObject, prop, null, propertyValue));
         }
      }
   }

   private void callChangeListenerMethod(Object receiver, String methodName)
   {
      Class<?> clazz = receiver.getClass();
      try
      {
         Method addPropertyChangeListener = clazz.getMethod(methodName, PropertyChangeListener.class);
         addPropertyChangeListener.invoke(receiver, this);
      }
      catch (Exception e)
      {
         // just skip it
      }
   }

   @Override
   public void propertyChange(PropertyChangeEvent evt)
   {
      if (this.closed)
      {
         return;
      }

      final Object newValue = evt.getNewValue();
      if (newValue != null)
      {
         this.subscribeTo(newValue);
      }

      this.elementListener.propertyChange(evt);
   }
}
<file_sep>package org.fulib.yaml.testmodel;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Product  
{

   public static final String PROPERTY_description = "description";

   private String description;

   public String getDescription()
   {
      return description;
   }

   public Product setDescription(String value)
   {
      if (value == null ? this.description != null : ! value.equals(this.description))
      {
         String oldValue = this.description;
         this.description = value;
         firePropertyChange("description", oldValue, value);
      }
      return this;
   }

   protected PropertyChangeSupport listeners = null;

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

   public boolean addPropertyChangeListener(PropertyChangeListener listener)
   {
      if (listeners == null)
      {
         listeners = new PropertyChangeSupport(this);
      }
      listeners.addPropertyChangeListener(listener);
      return true;
   }

   public boolean addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
   {
      if (listeners == null)
      {
         listeners = new PropertyChangeSupport(this);
      }
      listeners.addPropertyChangeListener(propertyName, listener);
      return true;
   }

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

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

   @Override
   public String toString()
   {
      StringBuilder result = new StringBuilder();

      result.append(" ").append(this.getId());
      result.append(" ").append(this.getDescription());


      return result.substring(1);
   }

   public static final String PROPERTY_id = "id";

   private String id;

   public String getId()
   {
      return id;
   }

   public Product setId(String value)
   {
      if (value == null ? this.id != null : ! value.equals(this.id))
      {
         String oldValue = this.id;
         this.id = value;
         firePropertyChange("id", oldValue, value);
      }
      return this;
   }

   public static final java.util.ArrayList<Offer> EMPTY_offers = new java.util.ArrayList<Offer>()
   { @Override public boolean add(Offer value){ throw new UnsupportedOperationException("No direct add! Use xy.withOffers(obj)"); }};

   public static final String PROPERTY_offers = "offers";

   private java.util.ArrayList<Offer> offers = null;

   public java.util.ArrayList<Offer> getOffers()
   {
      if (this.offers == null)
      {
         return EMPTY_offers;
      }

      return this.offers;
   }

   public Product withOffers(Object... value)
   {
      if(value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withOffers(i);
            }
         }
         else if (item instanceof Offer)
         {
            if (this.offers == null)
            {
               this.offers = new java.util.ArrayList<Offer>();
            }
            if ( ! this.offers.contains(item))
            {
               this.offers.add((Offer)item);
               ((Offer)item).setProduct(this);
               firePropertyChange("offers", null, item);
            }
         }
         else throw new IllegalArgumentException();
      }
      return this;
   }

   public Product withoutOffers(Object... value)
   {
      if (this.offers == null || value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withoutOffers(i);
            }
         }
         else if (item instanceof Offer)
         {
            if (this.offers.contains(item))
            {
               this.offers.remove((Offer)item);
               ((Offer)item).setProduct(null);
               firePropertyChange("offers", item, null);
            }
         }
      }
      return this;
   }

   public void removeYou()
   {
      this.withoutOffers(this.getOffers().clone());


      this.withoutCustomers(this.getCustomers().clone());


   }

   public static final java.util.ArrayList<Customer> EMPTY_customers = new java.util.ArrayList<Customer>()
   { @Override public boolean add(Customer value){ throw new UnsupportedOperationException("No direct add! Use xy.withCustomers(obj)"); }};

   public static final String PROPERTY_customers = "customers";

   private java.util.ArrayList<Customer> customers = null;

   public java.util.ArrayList<Customer> getCustomers()
   {
      if (this.customers == null)
      {
         return EMPTY_customers;
      }

      return this.customers;
   }

   public Product withCustomers(Object... value)
   {
      if(value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withCustomers(i);
            }
         }
         else if (item instanceof Customer)
         {
            if (this.customers == null)
            {
               this.customers = new java.util.ArrayList<Customer>();
            }
            if ( ! this.customers.contains(item))
            {
               this.customers.add((Customer)item);
               ((Customer)item).withProducts(this);
               firePropertyChange("customers", null, item);
            }
         }
         else throw new IllegalArgumentException();
      }
      return this;
   }

   public Product withoutCustomers(Object... value)
   {
      if (this.customers == null || value==null) return this;
      for (Object item : value)
      {
         if (item == null) continue;
         if (item instanceof java.util.Collection)
         {
            for (Object i : (java.util.Collection) item)
            {
               this.withoutCustomers(i);
            }
         }
         else if (item instanceof Customer)
         {
            if (this.customers.contains(item))
            {
               this.customers.remove((Customer)item);
               ((Customer)item).withoutProducts(this);
               firePropertyChange("customers", item, null);
            }
         }
      }
      return this;
   }

}
<file_sep>package org.fulib.yaml;

import java.util.Objects;

/**
 * This class provides static utility methods for working with strings in general, as used by various implementations
 * in this library.
 */
public class StrUtil
{
   /**
    * Transforms the first character of the input string to uppercase, leaving the remaining characters as-is.
    *
    * @param string
    *    the input string
    *
    * @return the input string with the first character in uppercase
    */
   public static String cap(String string)
   {
      final StringBuilder builder = new StringBuilder(string);
      builder.setCharAt(0, Character.toUpperCase(builder.charAt(0)));
      return builder.toString();
   }

   /**
    * Checks if the two strings are equal, handling nulls correctly.
    *
    * @param word1
    *    the first string
    * @param word2
    *    the second string
    *
    * @return true if the two strings are equal or both null, false otherwise
    *
    * @deprecated since 1.2; use {@link Objects#equals(Object, Object)} instead
    */
   @Deprecated
   public static boolean stringEquals(String word1, String word2)
   {
      return Objects.equals(word1, word2);
   }

   /**
    * Transforms the first character of the input string to lowercase, leaving the remaining characters as-is.
    *
    * @param string
    *    the input string
    *
    * @return the input string with the first character in lowercase
    */
   public static String downFirstChar(String string)
   {
      final StringBuilder builder = new StringBuilder(string);
      builder.setCharAt(0, Character.toLowerCase(builder.charAt(0)));
      return builder.toString();
   }
}
<file_sep>rootProject.name = 'fulibYaml'
<file_sep># fulibYaml

[![Java CI](https://github.com/fujaba/fulibYaml/workflows/Java%20CI/badge.svg)](https://github.com/fujaba/fulibYaml/actions)
[![javadoc](https://javadoc.io/badge2/org.fulib/fulibYaml/javadoc.svg)](https://javadoc.io/doc/org.fulib/fulibYaml)

Yaml serialization for [fulib](https://github.com/fujaba/fulib) object models. Used for storage and communication.

## Installation

`build.gradle`:

```groovy
repositories {
   // ...
   mavenCentral()
}
```

```groovy
dependencies {
   // https://mvnrepository.com/artifact/org.fulib/fulibYaml
   implementation group: 'org.fulib', name: 'fulibYaml', version: '1.5.0'
}
```

## License

[MIT](LICENSE.md)
<file_sep>package org.fulib.yaml;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class YamlIdMap extends IdMap
{
   // =============== Constants ===============

   private static final String REMOVE = "remove";

   // =============== Fields ===============

   private String yaml;
   private boolean decodingPropertyChange;

   private Yamler yamler = new Yamler();

   private HashMap<String, String> attrTimeStamps = new HashMap<>();

   private String yamlChangeText;

   // =============== Constructors ===============

   /**
    * @param packageName
    *    the names of the package in which model classes reside
    *
    * @since 1.2
    */
   public YamlIdMap(String packageName)
   {
      this(new ReflectorMap(packageName));
   }

   /**
    * @param packageNames
    *    the names of the packages in which model classes reside
    */
   public YamlIdMap(String... packageNames)
   {
      this(new ReflectorMap(packageNames));
   }

   /**
    * @param packageNames
    *    the names of the packages in which model classes reside
    *
    * @since 1.2
    */
   public YamlIdMap(Collection<String> packageNames)
   {
      this(new ReflectorMap(packageNames));
   }

   /**
    * @since 1.2
    */
   public YamlIdMap(ReflectorMap reflectorMap)
   {
      super(reflectorMap);
   }

   // =============== Properties ===============

   /**
    * @deprecated since 1.2; use {@link #getObject(String)} instead
    */
   @Deprecated
   public LinkedHashMap<String, Object> getObjIdMap()
   {
      return this.objIdMap;
   }

   /**
    * @deprecated since 1.2; use {@link #getId(Object)} instead
    */
   @Deprecated
   public LinkedHashMap<Object, String> getIdObjMap()
   {
      return this.idObjMap;
   }

   /**
    * @deprecated since 1.2; use {@link #getAttributeTimeStamp(String)} instead
    */
   @Deprecated
   public HashMap<String, String> getAttrTimeStamps()
   {
      return this.attrTimeStamps;
   }

   /**
    * @deprecated since 1.2; use {@link #setUserId(String)} instead
    */
   @Deprecated
   public YamlIdMap withUserId(String userId)
   {
      this.userId = userId;
      return this;
   }

   public boolean isDecodingPropertyChange()
   {
      return this.decodingPropertyChange;
   }

   public void setDecodingPropertyChange(boolean decodingPropertyChange)
   {
      this.decodingPropertyChange = decodingPropertyChange;
   }

   // =============== Methods ===============

   // --------------- CSV ---------------

   public Object decodeCSV(String fileName)
   {
      try
      {
         byte[] bytes = Files.readAllBytes(Paths.get(fileName));

         String csvText = new String(bytes);

         String yamlText = this.convertCsv2Yaml(csvText);

         // System.out.println(yamlText);

         return this.decode(yamlText);
      }
      catch (IOException e)
      {
         Logger.getGlobal().log(Level.SEVERE, e.getMessage(), e);
      }

      return null;
   }

   private String convertCsv2Yaml(String csvText)
   {
      String[] split = csvText.split(";");

      for (int i = 0; i < split.length; i++)
      {
         String token = split[i];

         if (token.startsWith("\"") && token.endsWith("\""))
         {
            // already done
            continue;
         }

         if (token.startsWith("\"") && !token.endsWith("\""))
         {
            // there is a semicolon within "   ;   " , recombine it
            int j = i;
            String nextToken;
            while (true)
            {
               j++;
               nextToken = split[j];
               split[j] = "";
               token = token + ";" + nextToken;
               if (nextToken.endsWith("\""))
               {
                  split[i] = token;
                  i = j;
                  break;
               }
            }
            continue;
         }

         if (token.trim().length() == 0)
         {
            continue;
         }

         Pattern pattern = Pattern.compile("\\s");
         Matcher matcher = pattern.matcher(token.trim());
         boolean found = matcher.find();

         if (found)
         {
            token = YamlGenerator.encapsulate(token);
            split[i] = token;
         }
      }

      StringBuilder buf = new StringBuilder();

      for (String str : split)
      {
         buf.append(str).append(" ");
      }

      return buf.toString();
   }

   // --------------- Decoding ---------------

   public Object decode(String yaml, Object root)
   {
      this.putObject(root);
      Object decodedRoot = this.decode(yaml);

      if (decodedRoot != root)
      {
         throw new RuntimeException("Object passed as root does not match the first object in the yaml string.\n"
                                    + "Ensure that the type of the passed root and the first object in the yaml string match. \n"
                                    + "Ensure that the key of the passed root and the key of the first object in tha yaml string match. \n"
                                    + "You get the key of the passed root object via 'String key = getOrCreateKey(root);'\n");
      }

      return root;
   }

   public Object decode(String yaml)
   {
      this.decodingPropertyChange = false;
      this.yamlChangeText = null;

      this.yaml = yaml;
      Object root;

      this.yamler = new Yamler(yaml);

      if (!"-".equals(this.yamler.getCurrentToken()))
      {
         return this.yamler.decode(yaml);
      }

      root = this.parseObjectIds();

      this.yamler = new Yamler(yaml);

      this.parseObjectAttrs();

      // reset property change decoding
      this.setDecodingPropertyChange(false);

      this.yamlChangeText = null;

      return root;
   }

   // --------------- Parsing ---------------

   private void parseObjectAttrs()
   {
      while (this.yamler.getCurrentToken() != null)
      {
         if (!"-".equals(this.yamler.getCurrentToken()))
         {
            this.yamler.printError("'-' expected");
            this.yamler.nextToken();
            continue;
         }

         String key = this.yamler.nextToken();

         if (key.endsWith(":"))
         {
            // usual
            this.parseUsualObjectAttrs();
         }
         else
         {
            this.parseObjectTableAttrs();
         }
      }
   }

   private void parseObjectTableAttrs()
   {
      // skip column names
      String className = this.yamler.getCurrentToken();

      Reflector creator = this.reflectorMap.getReflector(className);
      this.yamler.nextToken();

      ArrayList<String> colNameList = new ArrayList<>();

      while (this.yamler.getCurrentToken() != null && this.yamler.getLookAheadToken() != null
             && this.yamler.getLookAheadToken().endsWith(":"))
      {
         String colName = this.yamler.stripColon(this.yamler.getCurrentToken());
         colNameList.add(colName);
         this.yamler.nextToken();
      }

      while (this.yamler.getCurrentToken() != null && !"-".equals(this.yamler.getCurrentToken()))
      {
         String objectId = this.yamler.stripColon(this.yamler.getCurrentToken());
         this.yamler.nextToken();

         Object obj = this.objIdMap.get(objectId);

         // column values
         int colNum = 0;
         while (this.yamler.getCurrentToken() != null && !this.yamler.getCurrentToken().endsWith(":") && !"-".equals(
            this.yamler.getCurrentToken()))
         {
            String attrName = colNameList.get(colNum);

            if (this.yamler.getCurrentToken().startsWith("["))
            {
               String value = this.yamler.getCurrentToken().substring(1);
               if ("".equals(value.trim()))
               {
                  value = this.yamler.nextToken();
               }
               this.setValue(creator, obj, attrName, value);

               while (this.yamler.getCurrentToken() != null && !this.yamler.getCurrentToken().endsWith("]"))
               {
                  this.yamler.nextToken();
                  value = this.yamler.getCurrentToken();
                  if (this.yamler.getCurrentToken().endsWith("]"))
                  {
                     value = this.yamler.getCurrentToken().substring(0, this.yamler.getCurrentToken().length() - 1);
                  }
                  if (!"".equals(value.trim()))
                  {
                     this.setValue(creator, obj, attrName, value);
                  }
               }
            }
            else
            {
               this.setValue(creator, obj, attrName, this.yamler.getCurrentToken());
            }
            colNum++;
            this.yamler.nextToken();
         }
      }
   }

   private void parseUsualObjectAttrs()
   {
      String objectId = this.yamler.stripColon(this.yamler.getCurrentToken());
      String className = this.yamler.nextToken();
      this.yamler.nextToken();

      if (className.endsWith(".remove"))
      {
         this.objIdMap.remove(objectId);

         // skip time stamp, if necessary
         while (this.yamler.getCurrentToken() != null && !"-".equals(this.yamler.getCurrentToken()))
         {
            this.yamler.nextToken();
         }
         return;
      }

      if (".Map".equals(className))
      {
         YamlObject yamlObj = (YamlObject) this.objIdMap.get(objectId);
         Map<String, Object> map = yamlObj.getProperties();

         while (this.yamler.getCurrentToken() != null && !"-".equals(this.yamler.getCurrentToken()))
         {
            String attrName = this.yamler.stripColon(this.yamler.getCurrentToken());
            this.yamler.nextToken();

            if (map == null)
            {
               // no object created by parseObjectIds. Object has been removed.
               // ignore attr changes
               while (this.yamler.getCurrentToken() != null && !this.yamler.getCurrentToken().endsWith(":")
                      && !"-".equals(this.yamler.getCurrentToken()))
               {
                  this.yamler.nextToken();
               }
               continue;
            }

            // many values
            ArrayList<Object> previousValue = null;
            while (this.yamler.getCurrentToken() != null && !this.yamler.getCurrentToken().endsWith(":") && !"-".equals(
               this.yamler.getCurrentToken()))
            {
               String attrValue = this.yamler.getCurrentToken();

               Object target = this.objIdMap.get(attrValue);

               if (target != null)
               {
                  if (previousValue != null)
                  {
                     previousValue.add(target);
                     map.put(attrName, previousValue);
                  }
                  else
                  {
                     map.put(attrName, target);
                     previousValue = new ArrayList<>();
                     previousValue.add(target);
                  }
               }
               else
               {
                  if (previousValue != null)
                  {
                     previousValue.add(attrValue);
                     map.put(attrName, previousValue);
                  }
                  else
                  {
                     map.put(attrName, attrValue);
                     previousValue = new ArrayList<>();
                     previousValue.add(attrValue);
                  }
               }

               this.yamler.nextToken();
            }
         }
      }
      else
      {
         Reflector reflector = this.reflectorMap.getReflector(className);

         Object obj = this.objIdMap.get(objectId);

         // read attributes
         while (this.yamler.getCurrentToken() != null && !"-".equals(this.yamler.getCurrentToken()))
         {
            String attrName = this.yamler.stripColon(this.yamler.getCurrentToken());
            this.yamler.nextToken();

            if (obj == null)
            {
               // no object created by parseObjectIds. Object has been removed.
               // ignore attr changes
               while (this.yamler.getCurrentToken() != null && !this.yamler.getCurrentToken().endsWith(":")
                      && !"-".equals(this.yamler.getCurrentToken()))
               {
                  this.yamler.nextToken();
               }
               continue;
            }

            // many values
            while (this.yamler.getCurrentToken() != null && !this.yamler.getCurrentToken().endsWith(":") && !"-".equals(
               this.yamler.getCurrentToken()))
            {
               String attrValue = this.yamler.getCurrentToken();

               if (this.yamler.getLookAheadToken() != null && this.yamler.getLookAheadToken().endsWith(".time:"))
               {
                  String propWithTime = this.yamler.nextToken();
                  String newTimeStamp = this.yamler.nextToken();
                  String oldTimeStamp = this.attrTimeStamps.get(objectId + "." + attrName);

                  if (oldTimeStamp == null || oldTimeStamp.compareTo(newTimeStamp) <= 0)
                  {
                     this.setDecodingPropertyChange(true);

                     if (this.yamlChangeText == null)
                     {
                        this.yamlChangeText = this.yaml;
                     }

                     this.setValue(reflector, obj, attrName, attrValue);
                     this.attrTimeStamps.put(objectId + "." + attrName, newTimeStamp);
                  }
               }
               else
               {
                  this.setValue(reflector, obj, attrName, attrValue);
               }

               this.yamler.nextToken();
            }
         }
      }
   }

   private void setValue(Reflector reflector, Object obj, String attrName, String attrValue)
   {
      String type = "new";

      if (attrName.endsWith(".remove"))
      {
         attrName = attrName.substring(0, attrName.length() - ".remove".length());

         if (reflector.getValue(obj, attrName) instanceof Collection)
         {
            type = REMOVE;
         }
         else
         {
            attrValue = null;
         }
      }

      try
      {
         Object setResult = reflector.setValue(obj, attrName, attrValue, type);

         if (setResult == null)
         {
            Object targetObj = this.objIdMap.get(attrValue);
            if (targetObj != null)
            {
               reflector.setValue(obj, attrName, targetObj, type);
            }
         }
      }
      catch (Exception e)
      {
         // maybe a node
         Object targetObj = this.objIdMap.get(attrValue);
         if (targetObj != null)
         {
            reflector.setValue(obj, attrName, targetObj, type);
         }
      }
   }

   private Object parseObjectIds()
   {
      Object root = null;
      while (this.yamler.getCurrentToken() != null)
      {
         if (!"-".equals(this.yamler.getCurrentToken()))
         {
            this.yamler.printError("'-' expected");
            this.yamler.nextToken();
            continue;
         }

         String key = this.yamler.nextToken();

         if (key.endsWith(":"))
         {
            // usual
            Object now = this.parseUsualObjectId();
            if (root == null)
            {
               root = now;
            }
         }
         else
         {
            Object now = this.parseObjectTableIds();
            if (root == null)
            {
               root = now;
            }
         }
      }

      return root;
   }

   private Object parseUsualObjectId()
   {
      String objectId = this.yamler.stripColon(this.yamler.getCurrentToken());
      int pos = objectId.lastIndexOf('.');
      String numPart = objectId.substring(pos + 2);
      int objectNum;

      try
      {
         objectNum = Integer.parseInt(numPart);
      }
      catch (NumberFormatException e)
      {
         objectNum = this.objIdMap.size() + 1;
      }

      if (objectNum > this.maxUsedIdNum)
      {
         this.maxUsedIdNum = objectNum;
      }

      String className = this.yamler.nextToken();

      Object obj = this.objIdMap.get(objectId);

      String userId = null;

      // skip attributes
      while (this.yamler.getCurrentToken() != null && !"-".equals(this.yamler.getCurrentToken()))
      {
         String token = this.yamler.nextToken();
         if (token != null && token.endsWith(".time:"))
         {
            token = this.yamler.nextToken();

            userId = token.substring(token.lastIndexOf('.') + 1);
         }
      }

      boolean foreignChange = false;

      if (userId != null)
      {
         int dotIndex = objectId.indexOf('.');

         if (dotIndex > 0)
         {
            String ownerId = objectId.substring(0, dotIndex);
            foreignChange = !userId.equals(ownerId);
         }
      }

      if (obj == null && !className.endsWith(".remove") && !foreignChange)
      {
         if (".Map".equals(className))
         {
            obj = new YamlObject(objectId);
         }
         else
         {
            Reflector reflector = this.reflectorMap.getReflector(className);
            obj = reflector.newInstance();
         }

         this.objIdMap.put(objectId, obj);
         this.idObjMap.put(obj, objectId);
      }

      return obj;
   }

   private Object parseObjectTableIds()
   {
      Object root = null;

      // skip column names
      String className = this.yamler.getCurrentToken();

      Reflector reflector = this.reflectorMap.getReflector(className);

      while (!"".equals(this.yamler.getCurrentToken()) && this.yamler.getLookAheadToken().endsWith(":"))
      {
         this.yamler.nextToken();
      }

      while (!"".equals(this.yamler.getCurrentToken()) && !"-".equals(this.yamler.getCurrentToken()))
      {
         String objectId = this.yamler.stripColon(this.yamler.getCurrentToken());
         this.yamler.nextToken();

         Object obj = reflector.newInstance();

         this.objIdMap.put(objectId, obj);
         this.idObjMap.put(obj, objectId);

         if (root == null)
         {
            root = obj;
         }

         // skip column values
         while (!"".equals(this.yamler.getCurrentToken()) && !this.yamler.getCurrentToken().endsWith(":")
                && !"-".equals(this.yamler.getCurrentToken()))
         {
            this.yamler.nextToken();
         }
      }

      return root;
   }

   // --------------- Object Access ---------------

   /**
    * Puts the {@code object} in this IdMap with the specified {@code id}.
    * The call
    *
    * <pre><code>
    *     idMap.putNameObject("foo", bar);
    * </code></pre>
    * <p>
    * is equivalent to
    *
    * <pre><code>
    *     idMap.putObject("foo", bar);
    *     idMap.discoverObjects(bar);
    * </code></pre>
    * <p>
    * and the latter should be used for clarity.
    *
    * @param id
    *    the id
    * @param object
    *    the object
    *
    * @return this instance, to allow method chaining
    *
    * @deprecated since 1.2; use {@link #putObject(String, Object)} and {@link #discoverObjects(Object)} instead
    */
   @Deprecated
   public YamlIdMap putNameObject(String id, Object object)
   {
      this.putObject(id, object);
      this.discoverObjects(object);
      return this;
   }

   // --------------- Keys ---------------

   /**
    * @deprecated since 1.2; use {@link #putObject(Object)} instead
    */
   @Deprecated
   public String getOrCreateKey(Object obj)
   {
      return this.putObject(obj);
   }

   // --------------- Object Collection ---------------

   /**
    * Discovers all objects reachable from the {@code roots} and within the packages specified in the constructor.
    *
    * @param roots
    *    the root objects
    *
    * @return a set of all discovered objects
    *
    * @see #discoverObjects(Object...)
    *
    * @deprecated since 1.2; use {@link #discoverObjects(Object...)} instead (unless the resulting set is needed)
    */
   @Deprecated
   public LinkedHashSet<Object> collectObjects(Object... roots)
   {
      final LinkedHashSet<Object> collectedObjects = new LinkedHashSet<>();
      this.reflectorMap.discoverObjects(roots, collectedObjects);
      for (final Object collectedObject : collectedObjects)
      {
         this.putObject(collectedObject);
      }
      return collectedObjects;
   }

   // --------------- Encoding ---------------

   /**
    * Encodes this IdMap to a Yaml string.
    * This method is merely a shorthand for calling {@link #discoverObjects(Object...)} and {@link #encode()}.
    * I.e.,
    *
    * <pre><code>
    *    String yaml = idMap.encode(foo, bar, baz);
    * </code></pre>
    * <p>
    * is equivalent to
    *
    * <pre><code>
    *    idMap.discoverObjects(foo, bar, baz);
    *    String yaml = idMap.encode();
    * </code></pre>
    *
    * @param roots
    *    the root objects
    *
    * @return this IdMap encoded as a Yaml string
    */
   public String encode(Object... roots)
   {
      Objects.requireNonNull(roots);
      this.collectObjects(roots);
      return this.encode();
   }

   /**
    * Encodes this IdMap to a Yaml string.
    *
    * @return this IdMap encoded as a Yaml string
    *
    * @since 1.2
    */
   public String encode()
   {
      StringBuilder buf = new StringBuilder();
      for (Entry<String, Object> entry : this.objIdMap.entrySet())
      {
         Object obj = entry.getValue();
         if (obj instanceof Enum)
         {
            continue;
         }

         String key = entry.getKey();
         String className = obj.getClass().getName();

         buf.append("- ").append(key).append(": \t").append(className).append("\n");

         // attrs
         Reflector creator = this.getReflector(obj);

         for (String prop : creator.getAllProperties())
         {
            Object value = creator.getValue(obj, prop);

            if (value == null)
            {
               continue;
            }

            if (value instanceof Collection)
            {
               if (((Collection<?>) value).isEmpty())
               {
                  continue;
               }

               buf.append("  ").append(prop).append(':');
               for (Object item : (Collection<?>) value)
               {
                  buf.append(" \t");
                  this.encodeValue(buf, item);
               }
               buf.append('\n');
            }
            else if (value instanceof Map)
            {
            }
            else
            {
               buf.append("  ").append(prop).append(": \t");
               this.encodeValue(buf, value);
               buf.append('\n');

               // add time stamp?
               if (this.userId != null)
               {
                  String timeKey = key + "." + prop;
                  String timeStamp = this.attrTimeStamps.get(timeKey);

                  if (timeStamp != null)
                  {
                     buf.append("  ").append(prop).append(".time: \t").append(timeStamp).append("\n");
                  }
               }
            }
         }
         buf.append("\n");
      }

      return buf.toString();
   }

   private void encodeValue(StringBuilder buf, Object value)
   {
      if (value instanceof Enum)
      {
         final Enum<?> enumValue = (Enum<?>) value;
         // <enumClass>.<constantName>
         buf.append(enumValue.getDeclaringClass().getName()).append('.').append(enumValue.name());
         return;
      }

      final String valueKey = this.idObjMap.get(value);
      if (valueKey != null)
      {
         buf.append(valueKey);
      }
      else if (value instanceof String)
      {
         try
         {
            YamlGenerator.encapsulate((String) value, buf);
         }
         catch (IOException ignored)
         {
         }
      }
      else
      {
         buf.append(value);
      }
   }

   /**
    * @deprecated since 1.2; unused
    */
   @Deprecated
   public void encodeAttrValue(StringBuilder buf, Object obj, String propertyName, Object value)
   {
      // already known?
      String key = this.putObject(obj);
      String className = obj.getClass().getSimpleName();
      buf.append("- ").append(key).append(": \t").append(className).append("\n");
      Class<?> valueClass = value.getClass();

      if (valueClass.getName().startsWith("java.lang.") || valueClass == String.class)
      {
         buf.append("  ")
            .append(propertyName)
            .append(": \t")
            .append(YamlGenerator.encapsulate(value.toString()))
            .append("\n");
         if (this.userId != null)
         {
            String now = "" + LocalDateTime.now() + "." + this.userId;
            buf.append("  ").append(propertyName).append(".time: \t").append(now).append("\n");
            this.attrTimeStamps.put(key + "." + propertyName, now);
         }
      }
      else
      {
         // value is an object
         String valueKey = this.putObject(value);

         buf.append("  ").append(propertyName).append(": \t").append(valueKey).append("\n");
         if (this.userId != null)
         {
            // add timestamp only for to-one assocs
            Reflector reflector = this.reflectorMap.getReflector(obj);
            String fieldName = propertyName;

            if (propertyName.endsWith(".remove"))
            {
               fieldName = propertyName.substring(0, propertyName.lastIndexOf('.'));
            }

            Object fieldValue = reflector.getValue(obj, fieldName);

            String now = LocalDateTime.now() + "." + this.userId;
            if (fieldValue instanceof Collection)
            {
               buf.append("  ")
                  .append(propertyName)
                  .append('.')
                  .append(valueKey)
                  .append(".time: \t")
                  .append(now)
                  .append("\n");
               this.attrTimeStamps.put(key + "." + propertyName + "." + valueKey, now);
            }
            else
            {
               buf.append("  ").append(propertyName).append(".time: \t").append(now).append("\n");
               this.attrTimeStamps.put(key + "." + propertyName, now);
            }
         }

         if (!propertyName.endsWith(".remove"))
         {
            buf.append("- ").append(valueKey).append(": \t").append(valueClass.getSimpleName()).append("\n");
         }
      }
   }

   // --------------- Yaml Change ---------------

   public String getYamlChange()
   {
      String result = this.yamlChangeText;
      this.yamlChangeText = "";
      return result;
   }

   // --------------- Time Stamps ---------------

   /**
    * @deprecated since 1.2; unused
    */
   @Deprecated
   public String getLastTimeStamps()
   {
      LinkedHashMap<String, String> user2TimeStampMap = this.getLastTimeStampMap();

      StringBuilder buf = new StringBuilder();
      for (Entry<String, String> e : user2TimeStampMap.entrySet())
      {
         buf.append(e.getValue()).append(" ");
      }

      return buf.toString();
   }

   /**
    * @since 1.2
    */
   public String getAttributeTimeStamp(String attribute)
   {
      return this.attrTimeStamps.get(attribute);
   }

   /**
    * @deprecated since 1.2; unused
    */
   @Deprecated
   public LinkedHashMap<String, String> getLastTimeStampMap()
   {
      LinkedHashMap<String, String> user2TimeStampMap = new LinkedHashMap<>();

      for (Entry<String, String> e : this.attrTimeStamps.entrySet())
      {
         String timeStamp = e.getValue();
         int pos = timeStamp.lastIndexOf('.');
         String userName = timeStamp.substring(pos + 1);
         String oldTimeStamp = user2TimeStampMap.get(userName);

         if (oldTimeStamp == null || oldTimeStamp.compareTo(timeStamp) < 0)
         {
            user2TimeStampMap.put(userName, timeStamp);
         }
      }
      return user2TimeStampMap;
   }

   /**
    * @deprecated since 1.2; unused
    */
   @Deprecated
   public LinkedHashMap<String, String> getLastTimeStampMap(String lastTimeStamps)
   {
      LinkedHashMap<String, String> user2TimeStampMap = new LinkedHashMap<>();

      String[] split = lastTimeStamps.split("\\s+");

      for (String s : split)
      {
         int pos = s.lastIndexOf('.');
         String user = s.substring(pos + 1);
         user2TimeStampMap.put(user, s);
      }

      return user2TimeStampMap;
   }
}
<file_sep>package org.fulib.yaml;

import java.io.IOException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;

public class EventSource
{
   // =============== Constants ===============

   public static final String EVENT_KEY       = ".eventKey";
   public static final String EVENT_TIMESTAMP = ".eventTimestamp";
   public static final String EVENT_TYPE      = "eventType";

   // =============== Fields ===============

   private final Yamler yamler = new Yamler();

   private final List<Consumer<? super Map<String, String>>> eventListeners = new ArrayList<>();

   private final Map<String, Long>                    keyToTimeStampMap   = new HashMap<>();
   private final SortedMap<Long, Map<String, String>> timeStampToEventMap = new TreeMap<>();

   private long lastEventTime;

   private long oldEventTimeStamp = 0;

   public DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");

   // =============== Properties ===============

   public long getLastEventTime()
   {
      return this.lastEventTime;
   }

   // =============== Methods ===============

   // --------------- Bulk Retrieval ---------------

   /**
    * Gets all events after or at the specified timestamp.
    *
    * @param since
    *    the timestamp
    *
    * @return all events after or at the specified timestamp
    *
    * @deprecated since 1.2; use {@link #getEvents(long)} instead
    */
   @Deprecated
   public SortedMap<Long, LinkedHashMap<String, String>> pull(long since)
   {
      return this.pull(since, (Function<Map.Entry<Long, LinkedHashMap<String, String>>, Boolean>) null);
   }

   /**
    * Gets all events after or at the specified timestamp that have any one of the relevant event types.
    *
    * @param since
    *    the timestamp
    * @param relevantEventTypes
    *    the relevant event types
    *
    * @return all events after or at the specified timestamp that have any one of the relevant event types
    *
    * @deprecated since 1.2; use {@link #getEvents(long, String...)} instead
    */
   @Deprecated
   public SortedMap<Long, LinkedHashMap<String, String>> pull(long since, String... relevantEventTypes)
   {
      final Set<String> eventTypes = new HashSet<>(Arrays.asList(relevantEventTypes));
      return this.pull(since, e -> eventTypes.contains(e.getValue().get(EVENT_KEY)));
   }

   /**
    * Gets all events after or at the specified timestamp that fulfill the given predicate.
    *
    * @param since
    *    the timestamp
    * @param filterOp
    *    the predicate on timestamp and event
    *
    * @return all events after or at the specified timestamp that fulfill the given predicate
    *
    * @deprecated since 1.2; use {@link #getEvents(long, BiPredicate)} instead
    */
   @Deprecated
   public SortedMap<Long, LinkedHashMap<String, String>> pull(long since,
      Function<Map.Entry<Long, LinkedHashMap<String, String>>, Boolean> filterOp)
   {
      final SortedMap<Long, Map<String, String>> tailMap = this.timeStampToEventMap.tailMap(since);
      final TreeMap<Long, LinkedHashMap<String, String>> resultMap = new TreeMap<>();

      for (Map.Entry<Long, Map<String, String>> entry : tailMap.entrySet())
      {
         final LinkedHashMap<String, String> linkedEvent = makeLinked(entry.getValue());

         if (filterOp == null || filterOp.apply(new AbstractMap.SimpleEntry<>(entry.getKey(), linkedEvent)))
         {
            resultMap.put(entry.getKey(), linkedEvent);
         }
      }

      return resultMap;
   }

   /**
    * Gets all events, sorted by timestamp.
    *
    * @return all events, sorted by timestamp
    *
    * @since 1.2
    */
   public SortedMap<Long, Map<String, String>> getEvents()
   {
      return Collections.unmodifiableSortedMap(this.timeStampToEventMap);
   }

   /**
    * Gets all events after or at the specified timestamp.
    *
    * @param since
    *    the timestamp
    *
    * @return all events after or at the specified timestamp
    *
    * @since 1.2
    */
   public SortedMap<Long, Map<String, String>> getEvents(long since)
   {
      return Collections.unmodifiableSortedMap(this.timeStampToEventMap.tailMap(since));
   }

   /**
    * Gets all events after or at the specified timestamp that have any one of the relevant event types.
    *
    * @param since
    *    the timestamp
    * @param relevantEventTypes
    *    the relevant event types
    *
    * @return all events after or at the specified timestamp that have any one of the relevant event types
    *
    * @since 1.2
    */
   public SortedMap<Long, Map<String, String>> getEvents(long since, String... relevantEventTypes)
   {
      final Set<String> eventTypes = new HashSet<>(Arrays.asList(relevantEventTypes));
      return this.getEvents(since, (k, v) -> eventTypes.contains(v.get(EVENT_KEY)));
   }

   /**
    * Gets all events after or at the specified timestamp that fulfill the given predicate.
    *
    * @param since
    *    the timestamp
    * @param filterOp
    *    the predicate on timestamp and event
    *
    * @return all events after or at the specified timestamp that fulfill the given predicate
    *
    * @since 1.2
    */
   public SortedMap<Long, Map<String, String>> getEvents(long since,
      BiPredicate<? super Long, ? super Map<String, String>> filterOp)
   {
      final SortedMap<Long, Map<String, String>> events = this.timeStampToEventMap.tailMap(since);
      if (filterOp == null)
      {
         return events;
      }

      final SortedMap<Long, Map<String, String>> result = new TreeMap<>();

      for (final Map.Entry<Long, Map<String, String>> entry : events.entrySet())
      {
         final Long key = entry.getKey();
         final Map<String, String> value = entry.getValue();
         if (filterOp.test(key, value))
         {
            result.put(key, value);
         }
      }

      return result;
   }

   // --------------- Single Events ---------------

   /**
    * Gets the newest event with the given key, or null if not found.
    *
    * @param eventKey
    *    the event key
    *
    * @return the newest event with the given key
    *
    * @deprecated since 1.2; use {@link #getNewestEvent(String)} instead
    */
   @Deprecated
   public LinkedHashMap<String, String> getEvent(String eventKey)
   {
      return makeLinked(this.getNewestEvent(eventKey));
   }

   /**
    * Gets the newest event with the given key, or null if not found.
    *
    * @param eventKey
    *    the event key
    *
    * @return the newest event with the given key
    *
    * @since 1.2
    */
   public Map<String, String> getNewestEvent(String eventKey)
   {
      final Long timeStamp = this.keyToTimeStampMap.get(eventKey);
      return timeStamp != null ? this.timeStampToEventMap.get(timeStamp) : null;
   }

   /**
    * Checks whether the given event was already overwritten, i.e. whether a newer event with the same key exists.
    *
    * @param event
    *    the event
    *
    * @return true if the event was overwritten, false otherwise.
    *
    * @deprecated since 1.2; use {@link #isOverwritten(Map)} instead
    */
   @Deprecated
   public boolean isOverwritten(LinkedHashMap<String, String> event)
   {
      return this.isOverwritten((Map<String, String>) event);
   }

   /**
    * Checks whether the given event was already overwritten, i.e. whether a newer event with the same key exists.
    *
    * @param event
    *    the event
    *
    * @return true if the event was overwritten, false otherwise.
    *
    * @since 1.2
    */
   public boolean isOverwritten(Map<String, String> event)
   {
      String eventKey = event.get(EVENT_KEY);
      String eventTimeTxt = event.get(EVENT_TIMESTAMP);

      Long storedTime = this.keyToTimeStampMap.get(eventKey);

      if (storedTime == null)
      {
         return false;
      }

      String storedTimeTxt = this.dateFormat.format(storedTime);

      return storedTimeTxt.compareTo(eventTimeTxt) >= 0;
   }

   // --------------- Modification ---------------

   public void addEventListener(Consumer<? super Map<String, String>> listener)
   {
      this.eventListeners.add(listener);
   }

   /**
    * Sets the timestamp to use for the next event added with {@link #append(Map)}.
    *
    * @param oldEventTimeStamp
    *    the old timestamp, as a string
    *
    * @return this instance, to allow method chaining
    *
    * @deprecated since 1.2; parse the string yourself and use {@link #setOldEventTimeStamp(long)}
    */
   @Deprecated
   public EventSource setOldEventTimeStamp(String oldEventTimeStamp)
   {
      if (oldEventTimeStamp == null)
      {
         return this; //========================
      }

      long oldTimeStamp = 0;
      try
      {
         oldTimeStamp = this.dateFormat.parse(oldEventTimeStamp).getTime();
      }
      catch (ParseException e)
      {
         e.printStackTrace();
      }

      return this.setOldEventTimeStamp(oldTimeStamp);
   }

   /**
    * Sets the timestamp to use for the next event added with {@link #append(Map)}.
    *
    * @param oldEventTimeStamp
    *    the old timestamp, as a string
    *
    * @return this instance, to allow method chaining
    *
    * @since 1.2
    */
   public EventSource setOldEventTimeStamp(long oldEventTimeStamp)
   {
      this.oldEventTimeStamp = oldEventTimeStamp;
      return this;
   }

   /**
    * Adds the given event to this event source.
    *
    * @param event
    *    the event
    *
    * @return this instance, to allow method chaining
    *
    * @deprecated since 1.2; use {@link #append(Map)} instead
    */
   @Deprecated
   public EventSource append(LinkedHashMap<String, String> event)
   {
      return this.append((Map<String, String>) event);
   }

   /**
    * Adds the given event to this event source.
    *
    * @param event
    *    the event
    *
    * @return this instance, to allow method chaining
    *
    * @since 1.2
    */
   public EventSource append(Map<String, String> event)
   {
      String timestampString;

      this.setOldEventTimeStamp(event.get(EVENT_TIMESTAMP));

      if (this.oldEventTimeStamp > this.lastEventTime)
      {
         this.lastEventTime = this.oldEventTimeStamp;
      }
      else
      {
         long currentTime = System.currentTimeMillis();
         if (currentTime > this.lastEventTime)
         {
            this.lastEventTime = currentTime;
         }
         else
         {
            this.lastEventTime++;
         }
      }
      timestampString = this.dateFormat.format(this.lastEventTime);
      this.oldEventTimeStamp = 0;

      event.put(EVENT_TIMESTAMP, timestampString);

      String key = event.get(EVENT_KEY);
      if (key != null)
      {
         Long oldNum = this.keyToTimeStampMap.get(key);
         if (oldNum != null)
         {
            this.timeStampToEventMap.remove(oldNum);
         }
      }

      this.keyToTimeStampMap.put(key, this.lastEventTime);
      this.timeStampToEventMap.put(this.lastEventTime, event);

      for (Consumer<? super Map<String, String>> listener : this.eventListeners)
      {
         listener.accept(event);
      }

      return this;
   }

   /**
    * Parses the string as a YAML object list and adds each object as an event via {@link #append(Map)}.
    *
    * @param yaml
    *    the YAML object list
    *
    * @return this instance, to allow method chaining
    *
    * @deprecated since 1.2; parse the YAML yourself and use {@link #append(Map)}
    */
   @Deprecated
   public EventSource append(String yaml)
   {
      if (yaml == null)
      {
         return this; //===========================================
      }

      ArrayList<LinkedHashMap<String, String>> list = this.yamler.decodeList(yaml);

      for (LinkedHashMap<String, String> event : list)
      {
         this.append(event);
      }

      return this;
   }

   // --------------- Conversion ---------------

   public String encodeYaml()
   {
      return YamlGenerator.serialize(this.timeStampToEventMap.values());
   }

   public void encodeYaml(Writer writer) throws IOException
   {
      YamlGenerator.serialize(this.timeStampToEventMap.values(), writer);
   }

   // =============== Static Methods ===============

   @Deprecated // only used by legacy methods
   private static LinkedHashMap<String, String> makeLinked(Map<String, String> event)
   {
      return event instanceof LinkedHashMap ? (LinkedHashMap<String, String>) event : new LinkedHashMap<>(event);
   }

   /**
    * Encodes the events as a list of YAML objects.
    *
    * @param events
    *    the events
    *
    * @return the encoded YAML object list
    *
    * @deprecated since 1.2; use {@link YamlGenerator#serialize(Iterable) YamlGenerator.encodeYaml}{@code (events.values())} instead
    */
   @Deprecated
   public static String encodeYaml(SortedMap<Long, ? extends Map<String, String>> events)
   {
      return YamlGenerator.serialize(events.values());
   }

   /**
    * Encodes the events as a list of YAML objects.
    *
    * @param events
    *    the events
    *
    * @return the encoded YAML object list
    *
    * @deprecated since 1.2; use {@link YamlGenerator#serialize(Iterable)} instead
    */
   @Deprecated
   public static String encodeYaml(List<? extends Map<String, String>> events)
   {
      return YamlGenerator.serialize(events);
   }

   /**
    * Encodes the event as a YAML object.
    *
    * @param event
    *    the event
    *
    * @return the encoded YAML object
    *
    * @deprecated since 1.2; use {@link YamlGenerator#serialize(Map)} instead
    */
   @Deprecated
   public static String encodeYaml(LinkedHashMap<String, String> event)
   {
      return YamlGenerator.serialize(event);
   }
}
<file_sep>package org.fulib.yaml;

import java.util.Set;

/**
 * A reflector specialized for {@link YamlObject} instances.
 */
public class YamlObjectReflector extends Reflector
{
   // =============== Fields ===============

   private YamlObject yamlObject;

   // =============== Constructors ===============

   /**
    * @param newObject
    *    the yaml object (must be a {@link YamlObject} instance)
    *
    * @deprecated since 1.2; use {@link #YamlObjectReflector(YamlObject)} instead
    */
   @Deprecated
   public YamlObjectReflector(Object newObject)
   {
      this((YamlObject) newObject);
   }

   /**
    * @param yamlObject
    *    the yaml object
    *
    * @since 1.2
    */
   public YamlObjectReflector(YamlObject yamlObject)
   {
      this.yamlObject = yamlObject;
   }

   // =============== Properties ===============

   @Override
   public Set<String> getOwnProperties()
   {
      return this.yamlObject.getProperties().keySet();
   }

   @Override
   public Set<String> getAllProperties()
   {
      return this.getOwnProperties();
   }

   // =============== Methods ===============

   @Override
   public Object newInstance()
   {
      return new YamlObject();
   }

   @Override
   public Object getValue(Object object, String attribute)
   {
      return this.yamlObject.get(attribute);
   }

   @Override
   public Object setValue(Object object, String attribute, Object value)
   {
      return this.yamlObject.put(attribute, value);
   }

   @Override
   public void removeObject(Object object)
   {
   }
}
<file_sep>package org.fulib.yaml;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.StringTokenizer;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
 * Yamler reads simple key value pairs in YAML syntax.
 * <p>
 *    Example:
 * <pre><code>
 *    String yaml = &quot;msgType: newPlayer\n&quot;
 *                + &quot;login: albert\n&quot;
 *                + &quot;colors: blue red \n&quot;;
 *
 *    Yamler yamler = new Yamler();
 *    Map&lt;String, String&gt; map = yamler.decode(yaml);
 *    // {msgType=newPlayer, login=albert, colors=blue red}
 * </code></pre>
 */
public class Yamler
{
   // =============== Constants ===============

   private static final int LEADING_CONTEXT_CHARS = 10;
   private static final int TRAILING_CONTEXT_CHARS = 20;

   // =============== Fields ===============

   private String yaml;
   private StringTokenizer tokenizer;
   private String lookAheadToken;
   private String currentToken;
   private int currentPos;
   private int lookAheadPos;

   // =============== Constructors ===============

   public Yamler()
   {
      // empty
   }

   public Yamler(String yaml)
   {
      this.setInput(yaml);
   }

   // =============== Properties ===============

   private void setInput(String yaml)
   {
      this.yaml = yaml;
      this.tokenizer = new StringTokenizer(yaml);
      this.lookAheadToken = null;
      this.nextToken();
      this.nextToken();
   }

   public String getCurrentToken()
   {
      return this.currentToken;
   }

   public int getCurrentPos()
   {
      return this.currentPos;
   }

   public String getLookAheadToken()
   {
      return this.lookAheadToken;
   }

   public int getLookAheadPos()
   {
      return this.lookAheadPos;
   }

   // =============== Methods ===============

   public LinkedHashMap<String, String> decode(String yaml)
   {
      LinkedHashMap<String, String> result = new LinkedHashMap<>();
      this.decode(yaml, result::put);
      return result;
   }

   /**
    * @since 1.2
    */
   public void decode(String yaml, BiConsumer<String, String> consumer)
   {
      this.setInput(yaml);

      while (this.currentToken != null && this.currentToken.endsWith(":"))
      {
         String key = this.stripColon(this.currentToken);

         this.nextToken();

         String value = "";
         int valueStart = this.currentPos;

         // many values
         while (this.currentToken != null && !this.currentToken.endsWith(":"))
         {
            value = yaml.substring(valueStart, this.currentPos + this.currentToken.length());

            this.nextToken();
         }

         consumer.accept(key, value);
      }
   }

   public ArrayList<LinkedHashMap<String, String>> decodeList(String yaml)
   {
      ArrayList<LinkedHashMap<String, String>> result = new ArrayList<>();
      this.decodeList(yaml, () -> {
         LinkedHashMap<String, String> map = new LinkedHashMap<>();
         result.add(map);
         return map::put;
      });
      return result;
   }

   /**
    * @since 1.2
    */
   public void decodeList(String yaml, Supplier<? extends BiConsumer<String, String>> supplier)
   {
      this.setInput(yaml);
      while ("-".equals(this.currentToken))
      {
         final BiConsumer<String, String> consumer = supplier.get();
         this.nextToken();
         while (this.currentToken != null && this.currentToken.endsWith(":"))
         {
            String key = this.stripColon(this.currentToken);
            this.nextToken();
            String value = this.currentToken;
            this.nextToken();
            consumer.accept(key, value);
         }
      }
   }

   public String nextToken()
   {
      this.currentToken = this.lookAheadToken;
      this.currentPos = this.lookAheadPos;

      if (this.tokenizer.hasMoreTokens())
      {

         this.lookAheadToken = this.tokenizer.nextToken();
         int currentLength = 0;
         if (this.currentToken != null)
         {
            currentLength = this.currentToken.length();
         }
         this.lookAheadPos = this.yaml.indexOf(this.lookAheadToken, this.lookAheadPos + currentLength);
         // lookAheadPos = scanner.match().start();
      }
      else
      {
         this.lookAheadToken = null;
      }

      if (this.lookAheadToken != null && this.lookAheadToken.startsWith("\""))
      {
         // get up to end of string
         int stringStartPos = this.lookAheadPos + 1;
         String subToken = this.lookAheadToken;
         //MatchResult match = scanner.match();
         int subTokenEnd = this.lookAheadPos + subToken.length();
         while (subTokenEnd < stringStartPos + 1
                || (!subToken.endsWith("\"") || subToken.endsWith("\\\"")) && this.tokenizer.hasMoreTokens())
         {
            subToken = this.tokenizer.nextToken();
            subTokenEnd = this.yaml.indexOf(subToken, subTokenEnd) + subToken.length();
         }

         this.lookAheadToken = this.yaml.substring(stringStartPos, subTokenEnd - 1).replace("\\\"", "\"");
      }

      return this.currentToken;
   }

   public String stripColon(String key)
   {
      String id = key;

      if (key.endsWith(":"))
      {
         id = key.substring(0, key.length() - 1);
      }
      else
      {
         this.printError("key does not end with ':' " + key);
      }

      return id;
   }

   /**
    * Encapsulates a YAML value by enclosing it in quotes ("), if necessary.
    *
    * @param value
    *    the YAML value to encapsulate
    *
    * @return the encapsulated YAML value
    *
    * @deprecated since 1.2; use {@link YamlGenerator#encapsulate(String)} instead
    */
   @Deprecated
   public static String encapsulate(String value)
   {
      return YamlGenerator.encapsulate(value);
   }

   void printError(String msg)
   {
      int startPos = this.currentPos - LEADING_CONTEXT_CHARS;
      if (startPos < 0)
      {
         startPos = 0;
      }

      int endPos = this.currentPos + TRAILING_CONTEXT_CHARS;
      if (endPos >= this.yaml.length())
      {
         endPos = this.yaml.length();
      }

      final String info =
         this.yaml.substring(startPos, this.currentPos) + "<--" + msg + "-->" + this.yaml.substring(this.currentPos,
                                                                                                    endPos);
      System.err.println(info);
   }
}
