/**
   * Validates a new table layout against a reference layout for mutual compatibility.
   *
   * @param reference the reference layout against which to validate.
   * @param layout the new layout to validate.
   * @throws IOException in case of an IO Error reading from the schema table. Throws
   *     InvalidLayoutException if the layouts are incompatible.
   */
  public void validate(KijiTableLayout reference, KijiTableLayout layout) throws IOException {

    final ProtocolVersion layoutVersion = ProtocolVersion.parse(layout.getDesc().getVersion());

    if (layoutVersion.compareTo(Versions.LAYOUT_VALIDATION_VERSION) < 0) {
      // Layout versions older than layout-1.3.0 do not require validation
      return;
    }

    // Accumulator for error messages which will be used to create an exception if errors occur.
    final List<String> incompatabilityMessages = Lists.newArrayList();

    // Iterate through all families/columns in the new layout,
    // find a potential matching reference family/column,
    // and validate the reader/writer schema sets.
    // If no matching family/column exists in the reference layout the newly create column is valid.
    for (FamilyLayout flayout : layout.getFamilies()) {
      final ColumnId lgid = flayout.getLocalityGroup().getId();

      LocalityGroupLayout refLGLayout = null;
      if (reference != null) {
        // If there is a reference layout, check for a locality group matching the ID of the LG for
        // this family.  Locality Group IDs should not change between layouts.
        final String refLGName = reference.getLocalityGroupIdNameMap().get(lgid);
        if (refLGName != null) {
          // If there is a matching reference LG get its layout by name.
          refLGLayout = reference.getLocalityGroupMap().get(refLGName);
        }
      }

      // The ColumnId of the FamilyLayout from the table layout.  Also matches the FamilyLayout for
      // this family in the reference layout if present.
      final ColumnId familyId = flayout.getId();

      if (flayout.isMapType()) {
        // If the family is map-type, get the CellSchema for all values in the family.
        final CellSchema cellSchema = flayout.getDesc().getMapSchema();

        FamilyLayout refFamilyLayout = null;
        if (refLGLayout != null) {
          // If there is a matching reference LG, check for the existence of this family.
          final String refFamilyName = refLGLayout.getFamilyIdNameMap().get(familyId);
          if (refFamilyName != null) {
            refFamilyLayout = refLGLayout.getFamilyMap().get(refFamilyName);
          }
        }

        if (refFamilyLayout != null) {
          if (refFamilyLayout.isMapType()) {
            // If the FamilyLayout from both table layouts are map type, compare their CellSchemas.
            final CellSchema refCellSchema = refFamilyLayout.getDesc().getMapSchema();

            incompatabilityMessages.addAll(
                addColumnNamestoIncompatibilityMessages(
                    flayout.getName(), null, validateCellSchema(refCellSchema, cellSchema)));
          } else if (refFamilyLayout.isGroupType()) {
            // If the FamilyLayout changed from group-type to map-type between table layout versions
            // that is an incompatible change.
            incompatabilityMessages.add(
                String.format(
                    "Family: %s changed from group-type to map-type.", refFamilyLayout.getName()));
          } else {
            throw new InternalKijiError(
                String.format(
                    "Family: %s is neither map-type nor group-type.", refFamilyLayout.getName()));
          }
        } else {
          // If the reference FamilyLayout is null this indicates a new family, which is inherently
          // compatible, but we still have to validate that the new readers and writers are
          // internally compatible.
          incompatabilityMessages.addAll(
              addColumnNamestoIncompatibilityMessages(
                  flayout.getName(), null, validateCellSchema(null, cellSchema)));
        }
      } else if (flayout.isGroupType()) {
        // Check for a matching family from the reference layout.
        FamilyLayout refFamilyLayout = null;
        if (refLGLayout != null) {
          final String refFamilyName = refLGLayout.getFamilyIdNameMap().get(familyId);
          if (refFamilyName != null) {
            refFamilyLayout = refLGLayout.getFamilyMap().get(refFamilyName);
          }
        }

        if (refFamilyLayout != null) {
          if (refFamilyLayout.isGroupType()) {
            // If there is a matching reference family and it is the same family type, iterate
            // through the columns checking schema compatibility.  Only checks columns from the new
            // layout because removed columns are inherently valid.
            for (ColumnLayout columnLayout : flayout.getColumns()) {
              final CellSchema cellSchema = columnLayout.getDesc().getColumnSchema();

              final String refColumnName =
                  refFamilyLayout.getColumnIdNameMap().get(columnLayout.getId());
              ColumnLayout refColumnLayout = null;
              if (refColumnName != null) {
                // If there is a column from the reference layout with the same column ID, get its
                // layout.
                refColumnLayout = refFamilyLayout.getColumnMap().get(refColumnName);
              }
              // If there is a column from the reference layout with the same column ID, get its
              // CellSchema.
              final CellSchema refCellSchema =
                  (refColumnLayout == null) ? null : refColumnLayout.getDesc().getColumnSchema();

              // If there is no matching column, refCellSchema will be null and this will only test
              // that the new reader and writer schemas are internally compatible.
              incompatabilityMessages.addAll(
                  addColumnNamestoIncompatibilityMessages(
                      flayout.getName(),
                      columnLayout.getName(),
                      validateCellSchema(refCellSchema, cellSchema)));
            }
          } else if (refFamilyLayout.isMapType()) {
            // If the FamilyLayout changed from map-type to group-type between table layout versions
            // that is an incompatible change.
            incompatabilityMessages.add(
                String.format(
                    "Family: %s changed from map-type to group-type.", refFamilyLayout.getName()));
          } else {
            throw new InternalKijiError(
                String.format(
                    "Family: %s is neither map-type nor group-type.", refFamilyLayout.getName()));
          }
        } else {
          // If the reference FamilyLayout is null this indicates a new family, which is inherently
          // compatible, but we still have to validate that the new readers and writers are
          // internally compatible.
          for (ColumnLayout columnLayout : flayout.getColumns()) {
            final CellSchema cellSchema = columnLayout.getDesc().getColumnSchema();
            incompatabilityMessages.addAll(
                addColumnNamestoIncompatibilityMessages(
                    flayout.getName(),
                    columnLayout.getName(),
                    validateCellSchema(null, cellSchema)));
          }
        }

      } else {
        throw new InternalKijiError(
            String.format("Family: %s is neither map-type nor group-type.", flayout.getName()));
      }
    }

    // If there were any incompatibility errors, throw an exception.
    if (incompatabilityMessages.size() != 0) {
      throw new InvalidLayoutSchemaException(incompatabilityMessages);
    }
  }
Exemple #2
0
    /**
     * Initializes a new specification for an Entity from an annotated Java class.
     *
     * @param klass Annotated Java class to derive an entity specification from.
     * @param kiji Kiji instance where to fetch entities from.
     * @throws IOException on I/O error.
     */
    public EntitySpec(Class<T> klass, Kiji kiji) throws IOException {
      mClass = klass;

      final KijiEntity entity = klass.getAnnotation(KijiEntity.class);
      Preconditions.checkArgument(
          entity != null, "Class '{}' has no @KijiEntity annotation.", klass);
      mTableName = entity.table();

      final KijiTable table = kiji.openTable(mTableName);
      try {
        final KijiTableLayout layout = table.getLayout();

        // TODO: Support deprecated RowKeyFormat?
        final RowKeyFormat2 rowKeyFormat = (RowKeyFormat2) layout.getDesc().getKeysFormat();

        final Map<String, RowKeyComponent> rkcMap = Maps.newHashMap();
        final Map<String, Integer> rkcIndexMap = Maps.newHashMap();
        for (int index = 0; index < rowKeyFormat.getComponents().size(); ++index) {
          final RowKeyComponent rkc = rowKeyFormat.getComponents().get(index);
          rkcMap.put(rkc.getName(), rkc);
          rkcIndexMap.put(rkc.getName(), index);
        }
        mRowKeyComponentMap = ImmutableMap.copyOf(rkcMap);
        mRowKeyComponentIndexMap = ImmutableMap.copyOf(rkcIndexMap);

        // --------------------------------------------------------------------
        // Parse fields with annotations from the entity class:

        final List<Field> columnFields = Lists.newArrayList();
        final List<Field> entityIdFields = Lists.newArrayList();

        for (final Field field : mClass.getDeclaredFields()) {
          final KijiColumn column = field.getAnnotation(KijiColumn.class);
          final EntityIdField eidField = field.getAnnotation(EntityIdField.class);

          if ((column != null) && (eidField != null)) {
            throw new IllegalArgumentException(
                String.format(
                    "Field '%s' cannot have both @KijiColumn and @EntityIdField annotations.",
                    field));

          } else if (column != null) {
            LOG.debug("Validating column field '{}'.", field);
            field.setAccessible(true);
            columnFields.add(field);

            final FamilyLayout flayout = layout.getFamilyMap().get(column.family());
            Preconditions.checkArgument(
                flayout != null,
                "Field '%s' maps to non-existing family '%s' from table '%s'.",
                field,
                column.family(),
                mTableName);

            if (column.qualifier().isEmpty()) {
              // Request for a map-type family:
              Preconditions.checkArgument(
                  flayout.isMapType(),
                  "Field '%s' maps to family '%s' from table '%s' which is not a map-type family.",
                  field,
                  column.family(),
                  mTableName);

              // Validate field type:
              if (column.pageSize() > 0) {
                Preconditions.checkArgument(
                    MapFamilyVersionIterator.class.isAssignableFrom(field.getType()),
                    "Fields mapped to map-type family with paging enabled must be "
                        + "MapFamilyVersionIterator, got '{}'.",
                    field.getType());
              } else {
                // TODO Validate type when no paging enabled on map-type family.
              }

            } else {
              // Request for a fully-qualified column:
              final ColumnLayout clayout = flayout.getColumnMap().get(column.qualifier());
              Preconditions.checkArgument(
                  flayout != null,
                  "Field '%s' maps to non-existing column '%s:%s' from table '%s'.",
                  field,
                  column.family(),
                  column.qualifier(),
                  mTableName);

              // Validate field type:
              if (column.pageSize() > 0) {
                Preconditions.checkArgument(
                    ColumnVersionIterator.class.isAssignableFrom(field.getType()),
                    "Fields mapped to column with paging enabled must be "
                        + "ColumnVersionIterator, got '{}'.",
                    field.getType());
              } else {
                // TODO Validate type when no paging enabled on the column.
              }
            }

          } else if (eidField != null) {
            LOG.debug("Validating entity ID field '{}'.", field);
            field.setAccessible(true);
            entityIdFields.add(field);

            final RowKeyComponent rkc = mRowKeyComponentMap.get(eidField.component());
            Preconditions.checkArgument(
                rkc != null,
                "Field '%s' maps to unknown entity ID component '%s'.",
                field,
                eidField.component());

          } else {
            LOG.debug("Ignoring field '{}' with no annotation.", field);
          }
        }

        mColumnFields = ImmutableList.copyOf(columnFields);
        mEntityIdFields = ImmutableList.copyOf(entityIdFields);

      } finally {
        table.release();
      }
    }