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

ucar.nc2.Group Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2;

import static java.util.Optional.ofNullable;
import static ucar.nc2.NetcdfFiles.reservedFullName;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.ma2.DataType;
import ucar.nc2.constants.CDM;
import ucar.nc2.util.EscapeStrings;
import ucar.nc2.util.Indent;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Collections;

/**
 * A logical collection of Variables, Attributes, and Dimensions.
 * The Groups in a Dataset form a hierarchical tree, like directories on a disk.
 * A Group has a name and optionally a set of Attributes.
 * There is always at least one Group in a dataset, the root Group, whose name is the empty string.
 * Immutable if setImmutable() was called.
 *
 * TODO Group will be immutable in 6.
 * TODO Group will not implement AttributeContainer in 6, use Group.attributes().
 * TODO Group will not extend CDMNode in 6.
 *
 * @author caron
 */
public class Group extends CDMNode implements AttributeContainer {

  @Deprecated
  static List collectPath(Group g) {
    List list = new ArrayList<>();
    while (g != null) {
      list.add(0, g);
      g = g.getParentGroup();
    }
    return list;
  }

  /**
   * Is this the root group?
   *
   * @return true if root group
   */
  public boolean isRoot() {
    return getParentGroup() == null;
  }

  /**
   * Get the Variables contained directly in this group.
   *
   * @return List of type Variable; may be empty, not null.
   *         TODO return ImmutableList
   */
  public List getVariables() {
    return variables;
  }

  /** @deprecated use findVariableLocal() */
  @Deprecated
  @Nullable
  public Variable findVariable(String varShortName) {
    return findVariableLocal(varShortName);
  }

  /**
   * Find the Variable with the specified (short) name in this group.
   *
   * @param varShortName short name of Variable within this group.
   * @return the Variable, or null if not found
   */
  @Nullable
  public Variable findVariableLocal(String varShortName) {
    if (varShortName == null)
      return null;

    for (Variable v : variables) {
      if (varShortName.equals(v.getShortName()))
        return v;
    }
    return null;
  }

  /**
   * Find the Variable with the specified (short) name in this group or a parent group.
   *
   * @param varShortName short name of Variable.
   * @return the Variable, or null if not found
   */
  @Nullable
  public Variable findVariableOrInParent(String varShortName) {
    if (varShortName == null)
      return null;

    Variable v = findVariableLocal(varShortName);
    Group parent = getParentGroup();
    if ((v == null) && (parent != null))
      v = parent.findVariableOrInParent(varShortName);
    return v;
  }

  /**
   * Look in this Group and in its nested Groups for a Variable with a String valued Attribute with the given name
   * and value.
   *
   * @param attName look for an Attribuite with this name.
   * @param attValue look for an Attribuite with this value.
   * @return the first Variable that matches, or null if none match.
   */
  @Nullable
  public Variable findVariableByAttribute(String attName, String attValue) {
    for (Variable v : getVariables()) {
      for (Attribute att : v.attributes())
        if (attName.equals(att.getShortName()) && attValue.equals(att.getStringValue()))
          return v;
    }
    for (Group nested : getGroups()) {
      Variable v = nested.findVariableByAttribute(attName, attValue);
      if (v != null)
        return v;
    }
    return null;
  }

  /**
   * Get the parent Group, or null if its the root group.
   * Not deprecated.
   */
  @SuppressWarnings("deprecated")
  @Nullable
  public Group getParentGroup() {
    return this.group;
  }

  /**
   * Get the full name of this object.
   * Certain characters are backslash escaped (see NetcdfFiles.getFullName(Group))
   * Not deprecated.
   * 
   * @return full name with backslash escapes
   */
  @SuppressWarnings("deprecated")
  public String getFullName() {
    return NetcdfFiles.makeFullName(this);
  }

  /**
   * Get the Groups contained directly in this Group.
   *
   * @return List of type Group; may be empty, not null.
   */
  public ImmutableList getGroups() {
    return ImmutableList.copyOf(groups);
  }

  /** Get the owning NetcdfFile */
  public NetcdfFile getNetcdfFile() {
    return ncfile;
  }

  /**
   * Retrieve the Group with the specified (short) name.
   *
   * @param groupShortName short name of the nested group you are looking for.
   * @return the Group, or null if not found
   */
  @Nullable
  public Group findGroupLocal(String groupShortName) {
    if (groupShortName == null)
      return null;
    // groupShortName = NetcdfFile.makeNameUnescaped(groupShortName);

    for (Group group : groups) {
      if (groupShortName.equals(group.getShortName()))
        return group;
    }

    return null;
  }

  /** @deprecated use findGroupLocal() */
  @Deprecated
  public Group findGroup(String groupShortName) {
    return findGroupLocal(groupShortName);
  }

  /**
   * Get the shared Dimensions contained directly in this group.
   *
   * @return List of type Dimension; may be empty, not null.
   *         TODO return ImmutableList
   */
  public List getDimensions() {
    return dimensions;
  }

  /**
   * Create a dimension list using dimension names. The dimension is searched for recursively in the parent groups.
   *
   * @param dimString : whitespace separated list of dimension names, or '*' for Dimension.UNKNOWN, or number for anon
   *        dimension. null or empty String is a scalar.
   * @return list of dimensions, will return ImmutableList<> in version 6
   * @throws IllegalArgumentException if cant find dimension or parse error.
   */
  public ImmutableList makeDimensionsList(String dimString) throws IllegalArgumentException {
    return Dimensions.makeDimensionsList(this::findDimension, dimString);
  }

  /**
   * Get the enumerations contained directly in this group.
   *
   * @return List of type EnumTypedef; may be empty, not null.
   */
  public ImmutableList getEnumTypedefs() {
    return ImmutableList.copyOf(enumTypedefs);
  }

  /**
   * Find a Dimension in this or a parent Group, matching on short name.
   *
   * @param name Dimension name.
   * @return the Dimension, or null if not found
   */
  @Nullable
  public Dimension findDimension(String name) {
    if (name == null)
      return null;
    // name = NetcdfFile.makeNameUnescaped(name);
    Dimension d = findDimensionLocal(name);
    if (d != null)
      return d;
    Group parent = getParentGroup();
    if (parent != null)
      return parent.findDimension(name);

    return null;
  }

  /**
   * Find a Dimension in this or a parent Group, using equals.
   *
   * @param dim Dimension .
   * @return the Dimension, or null if not found
   */
  @Nullable
  public Dimension findDimension(Dimension dim) {
    if (dim == null) {
      return null;
    }
    for (Dimension d : dimensions) {
      if (d.equals(dim)) {
        return d;
      }
    }
    Group parent = getParentGroup();
    if (parent != null) {
      return parent.findDimension(dim);
    }
    return null;
  }

  /**
   * Find a Dimension using its (short) name, in this group only
   *
   * @param shortName Dimension name.
   * @return the Dimension, or null if not found
   */
  @Nullable
  public Dimension findDimensionLocal(String shortName) {
    if (shortName == null)
      return null;
    for (Dimension d : dimensions) {
      if (shortName.equals(d.getShortName()))
        return d;
    }

    return null;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Attributes

  /** The attributes contained by this Group. */
  public AttributeContainer attributes() {
    return attributes;
  }

  /** Find the attribute by name, return null if not exist */
  @Nullable
  public Attribute findAttribute(String name) {
    return attributes.findAttribute(name);
  }

  /**
   * Find a String-valued Attribute by name (ignore case), return the String value of the Attribute.
   *
   * @return the attribute value, or defaultValue if not found
   */
  public String findAttributeString(String attName, String defaultValue) {
    return attributes.findAttributeString(attName, defaultValue);
  }

  /** @deprecated Use attributes() */
  @Deprecated
  public java.util.List getAttributes() {
    return AttributeContainer.filter(attributes, CDM.SPECIALS).getAttributes();
  }

  /** @deprecated Use attributes() */
  public boolean isEmpty() {
    return attributes.isEmpty();
  }

  /** @deprecated Use findAttributeString() */
  @Deprecated
  public Attribute findAttributeIgnoreCase(String name) {
    return attributes.findAttributeIgnoreCase(name);
  }

  /** @deprecated Use attributes().findAttributeDouble() */
  @Deprecated
  public double findAttributeDouble(String attName, double defaultValue) {
    return attributes.findAttributeDouble(attName, defaultValue);
  }

  /** @deprecated Use attributes().findAttributeInteger() */
  @Deprecated
  public int findAttributeInteger(String attName, int defaultValue) {
    return attributes.findAttributeInteger(attName, defaultValue);
  }

  /** @deprecated Use Group.builder() */
  @Deprecated
  public Attribute addAttribute(Attribute att) {
    return attributes.addAttribute(att);
  }

  /** @deprecated Use Group.builder() */
  @Deprecated
  public void addAll(Iterable atts) {
    attributes.addAll(atts);
  }

  /** @deprecated Use Group.builder() */
  @Deprecated
  public boolean remove(Attribute a) {
    return attributes.remove(a);
  }

  /** @deprecated Use Group.builder() */
  @Deprecated
  public boolean removeAttribute(String attName) {
    return attributes.removeAttribute(attName);
  }

  /** @deprecated Use Group.builder() */
  @Deprecated
  public boolean removeAttributeIgnoreCase(String attName) {
    return attributes.removeAttributeIgnoreCase(attName);
  }

  ////////////////////////////////////////////////////////////////////////

  /** Find a Enumeration in this Group, using its short name. */
  @Nullable
  public EnumTypedef findEnumeration(String name) {
    return findEnumeration(name, false); // Keep the old behavior
  }

  /** Find a Enumeration in this or optionally the parent Groups, using its short name. */
  @Nullable
  public EnumTypedef findEnumeration(String name, boolean searchup) {
    if (name == null)
      return null;
    // name = NetcdfFile.makeNameUnescaped(name);
    // search this group's EnumTypedefs
    for (EnumTypedef d : this.getEnumTypedefs()) {
      if (name.equals(d.getShortName()))
        return d;
    }
    if (searchup) {
      Group parent = getParentGroup();
      if (parent != null)
        return parent.findEnumeration(name, searchup);
    }
    return null;
  }

  /**
   * Locate an enum type definition that is structurally
   * similar to the template type def. The Enum names are ignored.
   *
   * @param template match this enum type def
   * @param searchup if true, then search this group and then parent groups.
   */
  public EnumTypedef findSimilarEnumTypedef(EnumTypedef template, boolean searchup) {
    assert (template != null);
    // search this group builders's EnumTypedefs but with constraint on name
    EnumTypedef ed = enumTypedefs.stream().filter(e -> e.equalsMapOnly(template)).findFirst().orElse(null);
    if (ed != null)
      return ed;
    // Optionally search parents
    if (searchup) {
      Group gb = getParentGroup();
      if (gb != null)
        ed = gb.findSimilarEnumTypedef(template, searchup);
      return ed;
    }
    return null;
  }

  /**
   * Get the common parent of this and the other group.
   * Cant fail, since the root group is always a parent of any 2 groups.
   *
   * @param other the other group
   * @return common parent of this and the other group
   */
  public Group commonParent(Group other) {
    if (isParent(other))
      return this;
    if (other.isParent(this))
      return other;
    while (!other.isParent(this))
      other = other.getParentGroup();
    return other;
  }

  /**
   * Is this a parent of the other Group?
   *
   * @param other another Group
   * @return true is it is equal or a parent
   */
  public boolean isParent(Group other) {
    while ((other != this) && (other.getParentGroup() != null))
      other = other.getParentGroup();
    return (other == this);
  }

  //////////////////////////////////////////////////////////////////////////////////////

  /**
   * Get String with name and attributes. Used in short descriptions like tooltips.
   *
   * @return name and attributes String.
   */
  public String getNameAndAttributes() {
    StringBuilder sbuff = new StringBuilder();
    sbuff.append("Group ");
    sbuff.append(getShortName());
    sbuff.append("\n");
    for (Attribute att : attributes) {
      sbuff.append("  ").append(getShortName()).append(":");
      sbuff.append(att);
      sbuff.append(";");
      sbuff.append("\n");
    }
    return sbuff.toString();
  }

  /**
   * CDL representation.
   *
   * @param strict if true, write in strict adherence to CDL definition.
   * @return CDL representation.
   * @deprecated use CDLWriter
   */
  @Deprecated
  public String writeCDL(boolean strict) {
    Formatter buf = new Formatter();
    writeCDL(buf, new Indent(2), strict);
    return buf.toString();
  }

  /** @deprecated use CDLWriter */
  @Deprecated
  void writeCDL(Formatter out, Indent indent, boolean strict) {
    boolean hasE = (!enumTypedefs.isEmpty());
    boolean hasD = (!dimensions.isEmpty());
    boolean hasV = (!variables.isEmpty());
    // boolean hasG = (groups.size() > 0);
    boolean hasA = (!Iterables.isEmpty(attributes));

    if (hasE) {
      out.format("%stypes:%n", indent);
      indent.incr();
      for (EnumTypedef e : enumTypedefs) {
        e.writeCDL(out, indent, strict);
        out.format("%n");
      }
      indent.decr();
      out.format("%n");
    }

    if (hasD) {
      out.format("%sdimensions:%n", indent);
      indent.incr();
      for (Dimension myd : dimensions) {
        myd.writeCDL(out, indent, strict);
        out.format("%n");
      }
      indent.decr();
    }

    if (hasV) {
      out.format("%svariables:%n", indent);
      indent.incr();
      for (Variable v : variables) {
        v.writeCDL(out, indent, false, strict);
        out.format("%n");
      }
      indent.decr();
    }

    for (Group g : groups) {
      String gname = strict ? NetcdfFiles.makeValidCDLName(g.getShortName()) : g.getShortName();
      out.format("%sgroup: %s {%n", indent, gname);
      indent.incr();
      g.writeCDL(out, indent, strict);
      indent.decr();
      out.format("%s}%n%n", indent);
    }

    // if (hasA && (hasE || hasD || hasV || hasG))
    // out.format("%n");

    if (hasA) {
      if (isRoot())
        out.format("%s// global attributes:%n", indent);
      else
        out.format("%s// group attributes:%n", indent);

      for (Attribute att : attributes) {
        // String name = strict ? NetcdfFile.escapeNameCDL(getShortName()) : getShortName();
        if (!CDM.isspecial(att)) {
          out.format("%s", indent);
          att.writeCDL(out, strict, null);
          out.format(";");
          if (!strict && (att.getDataType() != DataType.STRING))
            out.format(" // %s", att.getDataType());
          out.format("%n");
        }
      }
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////

  /**
   * Constructor
   *
   * @param ncfile NetcdfFile owns this Group
   * @param parent parent of Group. If null, this is the root Group.
   * @param shortName short name of Group.
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public Group(NetcdfFile ncfile, Group parent, String shortName) {
    super(shortName);
    this.ncfile = ncfile;
    this.attributes = new AttributeContainerMutable(shortName);
    setParentGroup(parent == null ? ncfile.getRootGroup() : parent);
  }

  /**
   * Set the Group's parent Group
   *
   * @param parent parent group.
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public void setParentGroup(Group parent) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    super.setParentGroup(parent == null ? ncfile.getRootGroup() : parent);
  }


  /**
   * Set the short name, converting to valid CDM object name if needed.
   *
   * @param shortName set to this value
   * @return valid CDM object name
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public String setName(String shortName) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    setShortName(shortName);
    return getShortName();
  }

  /**
   * Adds the specified shared dimension to this group.
   *
   * @param dim the dimension to add.
   * @throws IllegalStateException if this dimension is {@link #setImmutable() immutable}.
   * @throws IllegalArgumentException if {@code dim} isn't shared or a dimension with {@code dim}'s name already
   *         exists within the group.
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public void addDimension(Dimension dim) {
    if (immutable)
      throw new IllegalStateException("Cant modify");

    if (!dim.isShared()) {
      throw new IllegalArgumentException("Dimensions added to a group must be shared.");
    }

    if (findDimensionLocal(dim.getShortName()) != null)
      throw new IllegalArgumentException(
          "Dimension name (" + dim.getShortName() + ") must be unique within Group " + getShortName());

    dimensions.add(dim);
    dim.setGroup(this);
  }

  /**
   * Adds the specified shared dimension to this group, but only if another dimension with the same name doesn't
   * already exist.
   *
   * @param dim the dimension to add.
   * @return {@code true} if {@code dim} was successfully added to the group. Otherwise, {@code false} will be returned,
   *         meaning that a dimension with {@code dim}'s name already exists within the group.
   * @throws IllegalStateException if this dimension is {@link #setImmutable() immutable}.
   * @throws IllegalArgumentException if {@code dim} isn't shared.
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public boolean addDimensionIfNotExists(Dimension dim) {
    if (immutable)
      throw new IllegalStateException("Cant modify");

    if (!dim.isShared()) {
      throw new IllegalArgumentException("Dimensions added to a group must be shared.");
    }

    if (findDimensionLocal(dim.getShortName()) != null)
      return false;

    dimensions.add(dim);
    dim.setGroup(this);
    return true;
  }

  /**
   * Add a nested Group
   *
   * @param g add this Group.
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public void addGroup(Group g) {
    if (immutable)
      throw new IllegalStateException("Cant modify");

    if (findGroupLocal(g.getShortName()) != null)
      throw new IllegalArgumentException(
          "Group name (" + g.getShortName() + ") must be unique within Group " + getShortName());

    groups.add(g);
    g.setParentGroup(this); // groups are a tree - only one parent
  }

  /**
   * Add an Enumeration
   *
   * @param e add this Enumeration.
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public void addEnumeration(EnumTypedef e) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    if (e == null)
      return;
    e.setParentGroup(this);
    enumTypedefs.add(e);
  }

  /**
   * Add a Variable
   *
   * @param v add this Variable.
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public void addVariable(Variable v) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    if (v == null)
      return;

    if (findVariableLocal(v.getShortName()) != null) {
      // Variable other = findVariable(v.getShortName()); // debug
      throw new IllegalArgumentException(
          "Variable name (" + v.getShortName() + ") must be unique within Group " + getShortName());
    }

    variables.add(v);
    v.setParentGroup(this); // variable can only be in one group
  }

  /**
   * Remove an Dimension : uses the dimension hashCode to find it.
   *
   * @param d remove this Dimension.
   * @return true if was found and removed
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public boolean remove(Dimension d) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    return d != null && dimensions.remove(d);
  }

  /**
   * Remove an Attribute : uses the Group hashCode to find it.
   *
   * @param g remove this Group.
   * @return true if was found and removed
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public boolean remove(Group g) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    return g != null && groups.remove(g);
  }

  /**
   * Remove a Variable : uses the variable hashCode to find it.
   *
   * @param v remove this Variable.
   * @return true if was found and removed
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public boolean remove(Variable v) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    return v != null && variables.remove(v);
  }

  /**
   * remove a Dimension using its name, in this group only
   *
   * @param dimName Dimension name.
   * @return true if dimension found and removed
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public boolean removeDimension(String dimName) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    for (int i = 0; i < dimensions.size(); i++) {
      Dimension d = dimensions.get(i);
      if (dimName.equals(d.getShortName())) {
        dimensions.remove(d);
        return true;
      }
    }
    return false;
  }

  /**
   * remove a Variable using its (short) name, in this group only
   *
   * @param shortName Variable name.
   * @return true if Variable found and removed
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public boolean removeVariable(String shortName) {
    if (immutable)
      throw new IllegalStateException("Cant modify");
    for (int i = 0; i < variables.size(); i++) {
      Variable v = variables.get(i);
      if (shortName.equals(v.getShortName())) {
        variables.remove(v);
        return true;
      }
    }
    return false;
  }

  /**
   * Make this immutable.
   *
   * @return this
   * @deprecated Use Group.builder()
   */
  @Deprecated
  public Group setImmutable() {
    super.setImmutable();
    variables = Collections.unmodifiableList(variables);
    dimensions = Collections.unmodifiableList(dimensions);
    groups = Collections.unmodifiableList(groups);
    return this;
  }

  @Override
  public String toString() {
    return writeCDL(false);
  }

  /**
   * Instances which have same name and parent are equal.
   */
  @Override
  public boolean equals(Object oo) {
    if (this == oo)
      return true;
    if (!(oo instanceof Group))
      return false;
    Group og = (Group) oo;
    if (!getShortName().equals(og.getShortName()))
      return false;
    return !((getParentGroup() != null) && !getParentGroup().equals(og.getParentGroup()));
  }

  /**
   * Override Object.hashCode() to implement equals.
   */
  @Override
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      result = 37 * result + getShortName().hashCode();
      if (getParentGroup() != null)
        result = 37 * result + getParentGroup().hashCode();
      hashCode = result;
    }
    return hashCode;
  }

  /**
   * Create groups to ensure path is defined
   *
   * @param ncf the containing netcdf file object
   * @param path the path to the desired group
   * @param ignorelast true => ignore last element in the path
   * @return the Group, or null if not found
   * @deprecated will move to dap2 in ver6
   */
  @Deprecated
  public Group makeRelativeGroup(NetcdfFile ncf, String path, boolean ignorelast) {
    path = path.trim();
    path = path.replace("//", "/");
    boolean isabsolute = (path.charAt(0) == '/');
    if (isabsolute)
      path = path.substring(1);

    // iteratively create path
    String[] pieces = path.split("/");
    if (ignorelast)
      pieces[pieces.length - 1] = null;

    Group current = (isabsolute ? ncfile.getRootGroup() : this);
    for (String name : pieces) {
      if (name == null)
        continue;
      String clearname = NetcdfFile.makeNameUnescaped(name); // ??
      Group next = current.findGroupLocal(clearname);
      if (next == null) {
        next = new Group(ncf, current, clearname);
        current.addGroup(next);
      }
      current = next;
    }
    return current;
  }

  ////////////////////////////////////////////////////////////////
  // TODO make private final and immutable in 6
  protected NetcdfFile ncfile;
  protected List variables = new ArrayList<>();
  protected List dimensions = new ArrayList<>();
  protected List groups = new ArrayList<>();
  protected AttributeContainer attributes;
  protected List enumTypedefs = new ArrayList<>();
  private int hashCode;

  private Group(Builder builder, @Nullable Group parent) {
    super(builder.shortName);
    this.group = parent;
    this.ncfile = builder.ncfile;

    builder.dimensions.forEach(d -> d.setGroup(this)); // LOOK remove in 6
    this.dimensions = new ArrayList<>(builder.dimensions);
    this.enumTypedefs = new ArrayList<>(builder.enumTypedefs);

    // only the root group build() should be called, the rest get called recursively
    this.groups =
        builder.gbuilders.stream().map(g -> g.setNcfile(this.ncfile).build(this)).collect(Collectors.toList());

    builder.vbuilders.forEach(vb -> {
      // dont override ncfile if its been set.
      if (vb.ncfile == null) {
        vb.setNcfile(this.ncfile);
      }
    });
    for (Variable.Builder vb : builder.vbuilders) {
      Variable var = vb.build(this);
      this.variables.add(var);
    }

    this.attributes = builder.attributes;

    // This needs to go away in 6.
    this.dimensions.forEach(d -> d.setParentGroup(this));
    this.enumTypedefs.forEach(e -> e.setParentGroup(this));
  }

  /** Turn into a mutable Builder. Can use toBuilder().build() to copy. */
  public Builder toBuilder() {
    Builder builder = builder().setName(this.shortName).setNcfile(this.ncfile).addAttributes(this.attributes)
        .addDimensions(this.dimensions).addEnumTypedefs(this.enumTypedefs);

    this.groups.forEach(g -> builder.addGroup(g.toBuilder()));
    this.variables.forEach(v -> builder.addVariable(v.toBuilder()));

    return builder;
  }

  public static Builder builder() {
    return new Builder();
  }

  /** A builder of Groups. */
  public static class Builder {
    static private final Logger logger = LoggerFactory.getLogger(Builder.class);

    private @Nullable Group.Builder parentGroup; // null for root group; ignored during build()
    public List gbuilders = new ArrayList<>();
    public List> vbuilders = new ArrayList<>();
    public String shortName = "";
    private NetcdfFile ncfile; // set by NetcdfFile.build()
    private AttributeContainerMutable attributes = new AttributeContainerMutable("");
    private List dimensions = new ArrayList<>();
    public List enumTypedefs = new ArrayList<>();
    private boolean built;

    public Builder setParentGroup(@Nullable Group.Builder parentGroup) {
      this.parentGroup = parentGroup;
      return this;
    }

    public @Nullable Group.Builder getParentGroup() {
      return this.parentGroup;
    }

    public Builder addAttribute(Attribute att) {
      Preconditions.checkNotNull(att);
      attributes.addAttribute(att);
      return this;
    }

    public Builder addAttributes(Iterable atts) {
      Preconditions.checkNotNull(atts);
      attributes.addAll(atts);
      return this;
    }

    public AttributeContainerMutable getAttributeContainer() {
      return attributes;
    }

    /** Add Dimension with error if it already exists */
    public Builder addDimension(Dimension dim) {
      Preconditions.checkNotNull(dim);
      findDimensionLocal(dim.shortName).ifPresent(d -> {
        throw new IllegalArgumentException("Dimension '" + d.shortName + "' already exists");
      });
      dimensions.add(dim);
      return this;
    }

    /** Add Dimension if it doesnt already exist */
    public boolean addDimensionIfNotExists(Dimension dim) {
      Preconditions.checkNotNull(dim);
      if (!findDimensionLocal(dim.shortName).isPresent()) {
        dimensions.add(dim);
        return true;
      }
      return false;
    }

    /** Add Dimensions with error if any already exist */
    public Builder addDimensions(Collection dims) {
      Preconditions.checkNotNull(dims);
      dims.forEach(this::addDimension);
      return this;
    }

    /**
     * Replace dimension if it exists, else just add it.
     *
     * @return true if there was an existing dimension of that name
     */
    public boolean replaceDimension(Dimension dim) {
      Optional want = findDimensionLocal(dim.shortName);
      want.ifPresent(d -> dimensions.remove(d));
      addDimension(dim);
      return want.isPresent();
    }

    /**
     * Remove dimension, if it exists.
     *
     * @return true if there was an existing dimension of that name
     */
    public boolean removeDimension(String name) {
      Optional want = findDimensionLocal(name);
      want.ifPresent(d -> dimensions.remove(d));
      return want.isPresent();
    }

    /** Find Dimension local to this Group */
    public Optional findDimensionLocal(String name) {
      return dimensions.stream().filter(d -> d.shortName.equals(name)).findFirst();
    }

    /** Is the Dimension contained within this Group or a parent Group */
    public boolean contains(Dimension want) {
      Dimension have = dimensions.stream().filter(d -> d.equals(want)).findFirst().orElse(null);
      if (have != null) {
        return true;
      }
      if (this.parentGroup != null) {
        return this.parentGroup.contains(want);
      }
      return false;
    }

    /** Find Dimension in this Group or a parent Group */
    public Optional findDimension(String name) {
      if (name == null) {
        return Optional.empty();
      }
      Optional dopt = findDimensionLocal(name);
      if (dopt.isPresent()) {
        return dopt;
      }
      if (this.parentGroup != null)
        return this.parentGroup.findDimension(name);

      return Optional.empty();
    }

    public Iterable getDimensions() {
      return dimensions;
    }

    /** Add a nested Group. */
    public Builder addGroup(Group.Builder nested) {
      Preconditions.checkNotNull(nested);
      this.findGroupLocal(nested.shortName).ifPresent(g -> {
        throw new IllegalStateException("Nested group already exists " + nested.shortName);
      });
      gbuilders.add(nested);
      nested.setParentGroup(this);
      return this;
    }

    public Builder addGroups(Collection groups) {
      Preconditions.checkNotNull(groups);
      groups.addAll(groups);
      return this;
    }

    /**
     * Remove group, if it exists.
     *
     * @return true if there was an existing group of that name
     */
    public boolean removeGroup(String name) {
      Optional want = findGroupLocal(name);
      want.ifPresent(v -> gbuilders.remove(v));
      return want.isPresent();
    }

    public Optional findGroupLocal(String shortName) {
      return this.gbuilders.stream().filter(g -> g.shortName.equals(shortName)).findFirst();
    }

    /**
     * Find a subgroup of this Group, with the specified relative name.
     * An embedded "/" separates group names.
     * Can have a leading "/" only if this is the root group.
     *
     * @param relativeName eg "group/subgroup/wantGroup".
     * @return Group or empty if not found.
     */
    public Optional findGroupNested(String relativeName) {
      if (relativeName == null || relativeName.isEmpty()) {
        return (this.getParentGroup() == null) ? Optional.of(this) : Optional.empty();
      }

      Group.Builder g = this;
      StringTokenizer stoke = new StringTokenizer(relativeName, "/");
      while (stoke.hasMoreTokens()) {
        String groupName = NetcdfFiles.makeNameUnescaped(stoke.nextToken());
        Optional sub = g.findGroupLocal(groupName);
        if (!sub.isPresent()) {
          return Optional.empty();
        }
        g = sub.get();
      }
      return Optional.of(g);
    }

    /** Is this group a parent of the other group ? */
    public boolean isParent(Group.Builder other) {
      while ((other != this) && (other.parentGroup != null))
        other = other.parentGroup;
      return (other == this);
    }

    /** Find the common parent with the other group ? */
    public Group.Builder commonParent(Group.Builder other) {
      if (isParent(other))
        return this;
      if (other.isParent(this))
        return other;
      while (!other.isParent(this))
        other = other.parentGroup;
      return other;
    }

    /**
     * Locate an enum type definition that is structurally
     * similar to the template type def. The Enum names are ignored.
     * 
     * @param template match this enum type def
     * @param searchup if true, then search this group and then parent groups.
     */
    public Optional findSimilarEnumTypedef(EnumTypedef template, boolean searchup) {
      Optional ed = Optional.empty();
      assert (template != null);
      // search this group builders's EnumTypedefs but with constraint on name
      ed = this.enumTypedefs.stream().filter(e -> (e.equalsMapOnly(template))).findFirst();
      if (ed.isPresent())
        return ed;
      // Optionally search parents
      if (searchup) {
        Group.Builder gb = getParentGroup();
        if (gb != null)
          ed = gb.findSimilarEnumTypedef(template, searchup);
        if (ed.isPresent())
          return ed;
      }
      return Optional.empty();
    }

    /** Find a Enumeration in this Group Builder, using its short name. */
    public Optional findEnumTypedef(String name) {
      // Keep the old behavior
      return findEnumTypedef(name, false);
    }

    /**
     * Find a Enumeration in this or a parent Group Builder, using its short name.
     * 
     * @param name for which to search
     * @param searchup if true, then search this group and then parent groups.
     */
    public Optional findEnumTypedef(String name, boolean searchup) {
      if (name == null)
        return Optional.empty();
      // search this group builders's EnumTypedefs
      Optional ed = this.enumTypedefs.stream().filter(e -> e.shortName.equals(name)).findFirst();
      if (ed.isPresent())
        return ed;
      // Optionally search parents
      if (searchup) {
        Group.Builder gb = getParentGroup();
        if (gb != null)
          ed = gb.findEnumTypedef(name, searchup);
        if (ed.isPresent())
          return ed;
      }
      return Optional.empty();
    }

    public Builder addEnumTypedef(EnumTypedef typedef) {
      Preconditions.checkNotNull(typedef);
      enumTypedefs.add(typedef);
      return this;
    }

    public Builder addEnumTypedefs(Collection typedefs) {
      Preconditions.checkNotNull(typedefs);
      enumTypedefs.addAll(typedefs);
      return this;
    }

    /**
     * Add a EnumTypedef if it does not already exist.
     * Return new or existing.
     */
    public EnumTypedef findOrAddEnumTypedef(String name, Map map) {
      Optional opt = findEnumTypedef(name); // Find in this group
      if (opt.isPresent()) {
        return opt.get();
      } else {
        EnumTypedef enumTypedef = new EnumTypedef(name, map);
        addEnumTypedef(enumTypedef);
        return enumTypedef;
      }
    }

    /** Add a Variable, throw error if one of the same name if it exists. */
    public Builder addVariable(Variable.Builder variable) {
      Preconditions.checkNotNull(variable);
      findVariableLocal(variable.shortName).ifPresent(v -> {
        throw new IllegalArgumentException("Variable '" + v.shortName + "' already exists");
      });
      vbuilders.add(variable);
      variable.setParentGroupBuilder(this);
      return this;
    }

    /** Add Variables, throw error if one of the same name if it exists. */
    public Builder addVariables(Collection> vars) {
      vars.forEach(this::addVariable);
      return this;
    }

    /**
     * Replace variable of same name, if it exists, else just add it.
     * 
     * @return true if there was an existing variable of that name
     */
    public boolean replaceVariable(Variable.Builder vb) {
      Optional> want = findVariableLocal(vb.shortName);
      want.ifPresent(v -> vbuilders.remove(v));
      addVariable(vb);
      return want.isPresent();
    }

    /**
     * Remove variable, if it exists.
     *
     * @return true if there was an existing variable of that name
     */
    public boolean removeVariable(String name) {
      Optional> want = findVariableLocal(name);
      want.ifPresent(v -> vbuilders.remove(v));
      return want.isPresent();
    }

    public Optional> findVariableLocal(String name) {
      return vbuilders.stream().filter(v -> v.shortName.equals(name)).findFirst();
    }

    /**
     * Find a Variable, with the specified relative name. No structure members.
     * 
     * @param relativeName eg "group/subgroup/varname".
     */
    public Optional> findVariableNested(String relativeName) {
      if (relativeName == null || relativeName.isEmpty()) {
        return Optional.empty();
      }

      // break into groupNames and varName
      Group.Builder group = this;
      String varName = relativeName;
      int pos = relativeName.lastIndexOf('/');
      if (pos >= 0) {
        String groupNames = relativeName.substring(0, pos);
        varName = relativeName.substring(pos + 1);
        group = findGroupNested(groupNames).orElse(null);
      }

      return group == null ? Optional.empty() : group.findVariableLocal(varName);
    }

    /**
     * Find the Variable with the specified (short) name in this group or a parent group.
     *
     * @param varShortName short name of Variable.
     * @return the Variable or empty.
     */
    public Optional> findVariableOrInParent(String varShortName) {
      if (varShortName == null)
        return Optional.empty();

      Optional> vopt = findVariableLocal(varShortName);

      Group.Builder parent = getParentGroup();
      if (!vopt.isPresent() && (parent != null)) {
        vopt = parent.findVariableOrInParent(varShortName);
      }
      return vopt;
    }

    // Generally ncfile is set in NetcdfFile.build()
    public Builder setNcfile(NetcdfFile ncfile) {
      this.ncfile = ncfile;
      return this;
    }

    public Builder setName(String shortName) {
      this.shortName = NetcdfFiles.makeValidCdmObjectName(shortName);
      return this;
    }

    @Deprecated
    public NetcdfFile getNcfile() {
      return this.ncfile;
    }

    /** Make list of dimensions by looking in this Group or parent groups */
    public ImmutableList makeDimensionsList(String dimString) throws IllegalArgumentException {
      return Dimensions.makeDimensionsList(dimName -> this.findDimension(dimName).orElse(null), dimString);
    }

    /**
     * Make the full name of the this group.
     * TODO In light of CF groups, we may have to start full names with '/'
     */
    public String makeFullName() {
      if (parentGroup == null) {
        return "";
      }
      StringBuilder sbuff = new StringBuilder();
      appendGroupName(sbuff, this);
      return sbuff.toString();
    }

    private void appendGroupName(StringBuilder sbuff, Group.Builder g) {
      if (g == null || g.getParentGroup() == null) {
        return;
      }
      appendGroupName(sbuff, g.getParentGroup());
      sbuff.append(EscapeStrings.backslashEscape(g.shortName, reservedFullName));
      sbuff.append("/");
    }

    /** Remove the given dimension from this group and any subgroups */
    public void removeDimensionFromAllGroups(Group.Builder group, Dimension remove) {
      group.dimensions.removeIf(dim -> dim.equals(remove));
      group.gbuilders.forEach(g -> removeDimensionFromAllGroups(g, remove));
    }

    /** Make a multimap of Dimensions and all the variables that reference them, in this group and its nested groups. */
    public void makeDimensionMap(Group.Builder parent, Multimap> dimUsedMap) {
      for (Variable.Builder v : parent.vbuilders) {
        for (Dimension d : getDimensionsFor(parent, v)) {
          if (d.isShared()) {
            dimUsedMap.put(d, v);
          }
        }
      }
      for (Group.Builder g : parent.gbuilders) {
        makeDimensionMap(g, dimUsedMap);
      }
    }

    private List getDimensionsFor(Group.Builder gb, Variable.Builder vb) {
      // TODO: In 6.0 remove group field in dimensions, just use equals() to match.
      List dims = new ArrayList<>();
      for (Dimension dim : vb.getDimensions()) {
        if (dim.isShared()) {
          Dimension sharedDim = gb.findDimension(dim.getShortName()).orElse(null);
          if (sharedDim == null) {
            throw new IllegalStateException(String.format("Shared Dimension %s does not exist in a parent group", dim));
          } else {
            dims.add(sharedDim);
          }
        } else {
          dims.add(dim);
        }
      }
      return dims;
    }

    /** Build the root group, with parent = null. */
    public Group build() {
      return build(null);
    }

    /** Normally this is called by NetcdfFile.build() */
    Group build(@Nullable Group parent) {
      if (built)
        throw new IllegalStateException("Group was already built " + this.shortName);
      built = true;
      return new Group(this, parent);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy