/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.schemarepo;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link Subject} is a collection of mutually compatible Schemas. <br/>
* <br/>
* Validation of schemas is pluggable and each subject may have its own
* validation rules defined with its own {@link Validator} when registered with
* a {@link Repository}. To create a {@link Subject} that validates its schemas,
* use {@link #validatingSubject(Subject, ValidatorFactory)}. <br/>
* <br/>
* Caching of schemas is pluggable via
* {@link #cacheWith(Subject, SchemaEntryCache)}. A {@link Subject} can only
* cache the schema to id mappings, as other properties of a Subject are not
* safe to cache. <br/>
* <br/>
* A {@link Subject} has a few basic methods for interacting with Schemas:
* <li>
* {@link #register(String)} attempts to register a schema with the Subject,
* according to the validation rules of the Subject. The operation is idempotent
* -- the return value is the {@link SchemaEntry} corresponding to the schema
* String whether the schema existed before the operation or not.</li>
* <li>
* {@link #registerIfLatest(String, SchemaEntry)} attempts to register a schema
* with the Subject, according to the validation rules of the Subject. The
* operation succeeds only if the provided {@code latest} value is the current
* latest schema in the system, and returns null otherwise.</li>
* <li>
* {@link #lookupById(String)} looks up a schema by its id, and returns null if
* such a schema does not exist. Since the mapping from id to schema is
* immutable, this result is cacheable.</li>
* <li>
* {@link #lookupBySchema(String)} looks up an id for a schema, and returns null
* if no such schema exists. Since the mapping from id to schema is immutable,
* this result is cacheable.</li>
* <li>
* {@link #allEntries()} returns all the schema entries for the subject, ordered
* from most recent to oldest. The result is not cacheable, since additional
* entries may be added.</li>
*
*/
public abstract class Subject {
private final String name;
/**
* A {@link Subject} has a name. The name must not be null or empty, and
* cannot contain whitespace. If the name contains whitespace an
* {@link IllegalArgumentException} is thrown.
**/
protected Subject(String name) {
RepositoryUtil.validateSchemaOrSubject(name);
this.name = name;
}
/**
* Return the Name of the Subject. A Subject name can not contain whitespace,
* and must not be empty or null.
*/
public String getName() {
return name;
}
/**
* @return The {@link SubjectConfig} for this Subject
*/
public abstract SubjectConfig getConfig();
/**
* Indicates whether the keys generated by this subject can be expected to parse
* as an integer. This delegates all the way through to the backing store and
* is not configurable through the Repository/Subject API, since implementations
* of the backing store are what determines how keys are generated; the contract
* otherwise is merely that they are Strings and unique per subject.
*
* @return a boolean indicating if the IDs for this Subject are integers
*/
public abstract boolean integralKeys();
/**
* If the provided schema has already been registered in this subject, return
* the id.
*
* If the provided schema has not been registered in this subject, register it
* and return its id.
*
* Idempotent -- If two users simultaneously register the same schema, they
* will both get the same {@link SchemaEntry} result and succeed.
*
* @param schema
* The schema to register
* @return The id of the schema
* @throws SchemaValidationException
* If the schema change is not valid according the validation rules
* of the subject
*/
public abstract SchemaEntry register(String schema)
throws SchemaValidationException;
/**
* Register the provided schema only if the current latest schema matches the
* provided latest entry.
*
*
* @param schema
* The schema to register
* @param latest
* the entry that must match the current actual latest value in order
* to register this schema.
* @return The id of the schema, or null if latest does not match.
* @throws SchemaValidationException
* If the schema change is not valid according the validation rules
* of the subject
*/
public abstract SchemaEntry registerIfLatest(String schema, SchemaEntry latest)
throws SchemaValidationException;
/**
* Lookup the {@link SchemaEntry} for the given schema. Since the mapping of
* schema to id is immutable, this result can be cached.
*
* @param schema
* The schema to look up
* @return The SchemaEntry of the schema or null if the schema is not
* registered
*/
public abstract SchemaEntry lookupBySchema(String schema);
/**
* Lookup the {@link SchemaEntry} for the given subject by id. Since the
* mapping of schema to id is immutable the result can be cached.
*
* @param id
* the id of the schema to look up
* @return The SchemaEntry of the schema or null if no such schema is
* registered for the provided id
*/
public abstract SchemaEntry lookupById(String id);
/**
* Lookup the most recently registered schema for the given subject. This
* result is not cacheable, since the latest schema may change.
*
* @return The {@link SchemaEntry} or null if no schema is registered with
* this subject
*/
public abstract SchemaEntry latest();
/**
* List the ids of schemas registered with the given subject, ordered from
* most recent to oldest. This result is not cacheable, since the
* {@link SchemaEntry} in the subject may grow over time.
*
* @return the {@link SchemaEntry} objects in this subject, ordered from most
* recent to oldest.
*/
public abstract Iterable<SchemaEntry> allEntries();
/**
* @return The name of the {@link Subject}
*/
@Override
public String toString() {
return name;
}
/**
* Create a {@link Subject} that rejects modifications, throwing
* {@link IllegalStateException} if a modification is attempted
**/
public static final Subject readOnly(Subject subject) {
if (null == subject) {
return subject;
} else {
return new ReadOnlySubject(subject);
}
}
private static class ReadOnlySubject extends DelegatingSubject {
private ReadOnlySubject(Subject subject) {
super(subject);
}
@Override
public SchemaEntry register(String schema) throws SchemaValidationException {
throw new IllegalStateException("Cannot register, subject is read-only");
}
@Override
public SchemaEntry registerIfLatest(String schema, SchemaEntry latest) {
throw new IllegalStateException("Cannot register, subject is read-only");
}
}
/**
* Create a {@link Subject} that validates schemas as configured.
*/
public static Subject validatingSubject(Subject subject, ValidatorFactory factory) {
if (null == subject) {
return subject;
}
List<Validator> validators;
SubjectConfig config = subject.getConfig();
// if the validators key is not specified in the subject config, get the default ones.
// ensure that even an empty set is honored as "no validators"
if (config.get(SubjectConfig.VALIDATORS_KEY) != null)
validators = factory.getValidators(config.getValidators());
else
validators = factory.getValidators(factory.getDefaultSubjectValidators());
if (!validators.isEmpty()) {
return new ValidatingSubject(subject, new CompositeValidator(validators));
} else {
return subject;
}
}
private static final class CompositeValidator implements Validator {
private final ArrayList<Validator> validators;
private CompositeValidator(List<Validator> validators) {
this.validators = new ArrayList<Validator>(validators);
}
@Override
public void validate(String schemaToValidate,
Iterable<SchemaEntry> schemasInOrder) throws SchemaValidationException {
for(Validator v : validators) {
v.validate(schemaToValidate, schemasInOrder);
}
}
}
private static class ValidatingSubject extends DelegatingSubject {
protected final Validator validator;
private ValidatingSubject(Subject delegate, Validator validator) {
super(delegate);
this.validator = validator;
}
@Override
public SchemaEntry register(String schema) throws SchemaValidationException {
while (true) {
Iterable<SchemaEntry> schemaEntries = allEntries();
SchemaEntry actualLatest = null;
for (SchemaEntry entry : schemaEntries) {
actualLatest = entry;
break;
}
validator.validate(schema, schemaEntries);
SchemaEntry registered = super.registerIfLatest(schema, actualLatest);
// if registered is not null, it was successful
if (null != registered) {
return registered;
}
}
}
@Override
public SchemaEntry registerIfLatest(String schema, SchemaEntry latest)
throws SchemaValidationException {
Iterable<SchemaEntry> schemaEntries = allEntries();
SchemaEntry actualLatest = null;
for (SchemaEntry entry : schemaEntries) {
actualLatest = entry;
break;
}
if (actualLatest == latest
|| ((actualLatest != null) && actualLatest.equals(latest))) {
// they are equal, either both are null or they equal
validator.validate(schema, schemaEntries);
return super.registerIfLatest(schema, latest);
} else {
return null;
}
}
}
/**
* Create a {@link Subject} that caches id to schema mappings using the
* {@link SchemaEntryCache} provided.
*
* @param subject
* The {@link Subject} to wrap
* @param cache
* The {@link SchemaEntryCache} to cache with
* @return returns a {@link Subject} instance that caches {@link SchemaEntry}
* values with the cache provided, if and only if both parameters are
* not null. <br/>
* If the provided subject is null, returns null. If the provided
* cache is null, returns the provided subject without wrapping it.
*/
public static Subject cacheWith(Subject subject, SchemaEntryCache cache) {
return (null == subject || null == cache) ?
subject : new CachingSubject(subject, cache);
}
private static class CachingSubject extends DelegatingSubject {
private final SchemaEntryCache cache;
private CachingSubject(Subject delegate, SchemaEntryCache cache) {
super(delegate);
this.cache = cache;
}
@Override
public SchemaEntry register(String schema) throws SchemaValidationException {
SchemaEntry entry = cache.lookupBySchema(schema);
if (entry == null) {
return cache.add(super.register(schema));
}
return entry;
}
@Override
public SchemaEntry registerIfLatest(String schema, SchemaEntry latest)
throws SchemaValidationException {
return cache.add(super.registerIfLatest(schema, latest));
}
@Override
public SchemaEntry lookupBySchema(String schema) {
SchemaEntry entry = cache.lookupBySchema(schema);
if (entry == null) {
return cache.add(super.lookupBySchema(schema));
}
return entry;
}
@Override
public SchemaEntry lookupById(String id) {
SchemaEntry entry = cache.lookupById(id);
if (entry == null) {
return cache.add(super.lookupById(id));
}
return entry;
}
@Override
public Iterable<SchemaEntry> allEntries() {
Iterable<SchemaEntry> all = super.allEntries();
for (SchemaEntry entry : all) {
cache.add(entry);
}
return all;
}
}
}