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

de.codecamp.messages.shared.bundle.BundleFile Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
package de.codecamp.messages.shared.bundle;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.PropertiesConfiguration.JupIOFactory;
import org.apache.commons.configuration2.PropertiesConfigurationLayout;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.lang3.StringUtils;

import de.codecamp.messages.shared.bundle.MessageBundleManager.BundleFileCoordinates;


/**
 * Represents one file (i.e. one locale) of a message bundle.
 *
 * @param 
 *          the file reference
 */
public class BundleFile
  implements
    Comparable>
{

  private final FileSystemAdapter fileSystem;

  private final F file;

  private final String bundleName;

  private final Locale locale;

  private final String displayPath;

  private boolean readOnly = false;

  private final PropertiesConfiguration properties;

  private final FileHandler fileHandler;

  private boolean loaded = false;

  private boolean needsSaving;

  private Map lineNumbersCache;


  public BundleFile(FileSystemAdapter fileSystem, F file, Charset encoding,
      BundleFileCoordinates coordinates, String displayPath)
  {
    this.bundleName = coordinates.getBundleName();
    this.locale = coordinates.getLocale();

    this.fileSystem = fileSystem;
    this.file = file;
    this.displayPath = displayPath;

    this.properties = new PropertiesConfiguration();

    boolean escapeUnicode = !encoding.contains(StandardCharsets.UTF_8);
    this.properties.setIOFactory(new JupIOFactory(escapeUnicode));

    this.properties.setLayout(new SortedPropertiesConfigurationLayout());
    /*
     * Loading does not determine the line separator. So there's no way to (easily) preserve the
     * line separator, and saving without setting it will use the platform default.
     */
    properties.getLayout().setLineSeparator("\n");
    properties.getLayout().setForceSingleLine(true);
    properties.getLayout().setGlobalSeparator(" = ");

    fileHandler = new FileHandler(properties);
    fileHandler.setEncoding(encoding.name());
  }


  public F getLocation()
  {
    return file;
  }

  public String getDisplayPath()
  {
    return displayPath;
  }

  public String getBundleName()
  {
    return bundleName;
  }

  public Locale getLocale()
  {
    return locale;
  }

  public boolean isReadOnly()
  {
    return readOnly;
  }

  void setReadOnly(boolean readOnly)
  {
    this.readOnly = readOnly;
  }

  boolean isLoaded()
  {
    return loaded;
  }

  public boolean isEmpty()
  {
    checkLoaded();
    return properties.isEmpty();
  }


  public Set getKeys()
  {
    checkLoaded();
    return new HashSet<>(properties.getLayout().getKeys());
  }

  public boolean hasMessageKey(String messageKey)
  {
    checkLoaded();
    return properties.containsKey(messageKey);
  }

  public boolean isMessageEmpty(String messageKey)
  {
    return !hasMessage(messageKey);
  }

  public boolean hasMessage(String messageKey)
  {
    return StringUtils.isNotEmpty(getMessage(messageKey));
  }

  public String getMessage(String messageKey)
  {
    checkLoaded();
    return properties.getString(messageKey);
  }

  public void setMessage(String messageKey, String message)
  {
    checkLoaded();
    checkReadOnly();
    Object oldMessage = properties.getProperty(messageKey);
    properties.setProperty(messageKey, message);
    if (!Objects.equals(oldMessage, message))
    {
      needsSaving = true;
      lineNumbersCache = null;
    }
  }

  public String removeMessage(String messageKey)
  {
    checkLoaded();
    checkReadOnly();
    String message = null;
    if (properties.containsKey(messageKey))
    {
      message = properties.getString(messageKey);
      properties.clearProperty(messageKey);
      needsSaving = true;
      lineNumbersCache = null;
    }
    return message;
  }


  public String getComment(String messageKey)
  {
    checkLoaded();
    return properties.getLayout().getComment(messageKey);
  }

  public void setComment(String messageKey, String comment)
  {
    checkLoaded();
    checkReadOnly();
    String oldComment = properties.getLayout().getComment(messageKey);
    properties.getLayout().setComment(messageKey, comment);
    if (!Objects.equals(oldComment, comment))
    {
      needsSaving = true;
      lineNumbersCache = null;
    }
  }


  public Integer findLineNumberOfKey(String messageKey)
    throws BundleException
  {
    updateLineNumberOfKeys();

    return lineNumbersCache.get(messageKey);
  }

  public Map findLineNumberOfKeys(Collection messageKeys)
    throws BundleException
  {
    updateLineNumberOfKeys();

    Map lineNumbers = new HashMap<>();
    for (String messageKey : messageKeys)
    {
      Integer lineNumber = lineNumbersCache.get(messageKey);
      if (lineNumber != null)
        lineNumbers.put(messageKey, lineNumber);
    }
    return lineNumbers;
  }


  private void updateLineNumberOfKeys()
    throws BundleException
  {
    checkLoaded();
    if (needsSaving())
      throw new IllegalStateException("Save file before querying line numbers.");

    // cache is already filled and up-to-date
    if (lineNumbersCache != null)
      return;

    Map lineNumbers = new HashMap<>();

    List lines = new ArrayList<>();
    try (InputStream in = fileSystem.newInputStream(getLocation()))
    {
      BufferedReader reader =
          new BufferedReader(new InputStreamReader(in, fileHandler.getEncoding()));

      String line;
      while ((line = reader.readLine()) != null)
      {
        lines.add(line);
      }
    }
    catch (Exception ex)
    {
      String msg = "Failed to load message bundle file '%s'.";
      msg = String.format(msg, getDisplayPath());
      throw new BundleException(msg, ex);
    }

    Pattern pattern = Pattern.compile(
        "^\\s*(" + "([\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*[\\p{L}_$][\\p{L}\\p{N}_$]*" + ")\\s*=.*");

    for (int i = 0; i < lines.size(); i++)
    {
      String line = lines.get(i);
      Matcher matcher = pattern.matcher(line);
      if (matcher.matches())
      {
        lineNumbers.put(matcher.group(1), i + 1);
      }
    }

    lineNumbersCache = lineNumbers;
  }


  public boolean exists()
  {
    return fileSystem.exists(getLocation());
  }

  public boolean needsSaving()
  {
    return loaded && needsSaving;
  }

  public boolean needsSorting()
  {
    List keysSorted = new ArrayList<>(properties.getLayout().getKeys());
    List keysOriginal = new ArrayList<>(
        ((SortedPropertiesConfigurationLayout) properties.getLayout()).getUnsortedKeys());
    return !keysSorted.equals(keysOriginal);
  }

  public boolean save()
    throws BundleException
  {
    return save(false);
  }

  public boolean save(boolean force)
    throws BundleException
  {
    if (isReadOnly() || (!force && !needsSaving()))
      return false;
    checkReadOnly();

    try
    {
      fileSystem.createParentDirectories(getLocation());
    }
    catch (Exception ex)
    {
      String msg = "Failed to create directories for '%s'.";
      msg = String.format(msg, getLocation());
      throw new BundleException(msg, ex);
    }


    try (OutputStream out = fileSystem.newOutputStream(getLocation()))
    {
      fileHandler.save(out);
      needsSaving = false;
      return true;
    }
    catch (Exception ex)
    {
      String msg = "Failed to save message bundle file '%s'.";
      msg = String.format(msg, getDisplayPath());
      throw new BundleException(msg, ex);
    }
  }

  private void checkLoaded()
  {
    if (!loaded)
      throw new IllegalStateException("Bundle file has not been loaded.");
  }

  protected void ensureLoaded()
    throws BundleException
  {
    if (loaded)
      return;

    if (!exists())
    {
      loaded = true;
      needsSaving = true;
      return;
    }


    try (InputStream in = fileSystem.newInputStream(getLocation()))
    {
      fileHandler.load(in);
    }
    catch (Exception ex)
    {
      String msg = "Failed to load message bundle file '%s'.";
      msg = String.format(msg, getDisplayPath());
      throw new BundleException(msg, ex);
    }

    loaded = true;
    needsSaving = false;
  }

  public void delete()
    throws BundleException
  {
    checkReadOnly();
    try
    {
      fileSystem.deleteIfExists(getLocation());
    }
    catch (Exception ex)
    {
      String msg = "Failed to delete empty message bundle file '%s'.";
      msg = String.format(msg, getDisplayPath());
      throw new BundleException(msg, ex);
    }
  }

  private void checkReadOnly()
  {
    if (readOnly)
    {
      throw new IllegalStateException(
          String.format("Message bundle '%s' is read-only.", bundleName));
    }
  }


  @Override
  public int compareTo(BundleFile o)
  {
    int result = getBundleName().compareTo(o.getBundleName());
    if (result != 0)
      return result;
    return getLocale().toLanguageTag().compareTo(o.getLocale().toLanguageTag());
  }


  static class SortedPropertiesConfigurationLayout
    extends PropertiesConfigurationLayout
  {
    @Override
    public Set getKeys()
    {
      return new TreeSet<>(super.getKeys());
    }

    public Set getUnsortedKeys()
    {
      return super.getKeys();
    }
  };

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy