blob: fb64ccf015afaae1a90455ad47c3a9aae578d2fc [file] [log] [blame]
/*
* 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.apache.rya.indexing.entity.model;
import static java.util.Objects.requireNonNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import org.apache.http.annotation.Immutable;
import org.apache.log4j.Logger;
import org.apache.rya.api.domain.RyaURI;
import org.apache.rya.indexing.entity.storage.EntityStorage;
import org.apache.rya.indexing.smarturi.SmartUriAdapter;
import org.apache.rya.indexing.smarturi.SmartUriException;
import org.eclipse.rdf4j.model.IRI;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import edu.umd.cs.findbugs.annotations.DefaultAnnotation;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
/**
* An {@link Entity} is a named concept that has at least one defined structure
* and a bunch of values that fit within each of those structures. A structure is
* defined by a {@link Type}. A value that fits within that Type is a {@link Property}.
* </p>
* For example, suppose we want to represent a type of icecream as an Entity.
* First we must define what properties an icecream entity may have:
* <pre>
* Type ID: &lt;urn:icecream>
* Properties: &lt;urn:brand>
* &lt;urn:flavor>
* &lt;urn:ingredients>
* &lt;urn:nutritionalInformation>
* </pre>
* Now we can represent our icecream whose brand is "Awesome Icecream" and whose
* flavor is "Chocolate", but has no ingredients or nutritional information, as
* an Entity by doing the following:
* <pre>
* final Entity entity = Entity.builder()
* .setSubject(new RyaURI("urn:GTIN-14/00012345600012"))
* .setExplicitType(new RyaURI("urn:icecream"))
* .setProperty(new RyaURI("urn:icecream"), new Property(new RyaURI("urn:brand"), new RyaType(XMLSchema.STRING, "Awesome Icecream")))
* .setProperty(new RyaURI("urn:icecream"), new Property(new RyaURI("urn:flavor"), new RyaType(XMLSchema.STRING, "Chocolate")))
* .build();
* </pre>
* The two types of Entities that may be created are implicit and explicit.
* An implicit Entity is one who has at least one {@link Property} that matches
* the {@link Type}, but nothing has explicitly indicated it is of that Type.
* Once something has done so, it is an explicitly typed Entity.
*/
@Immutable
@DefaultAnnotation(NonNull.class)
public class Entity {
private static final Logger log = Logger.getLogger(Entity.class);
private final RyaURI subject;
private final ImmutableList<RyaURI> explicitTypeIds;
// First key is Type ID.
// Second key is Property Name.
// Value is the Property value for a specific type.
private final ImmutableMap<RyaURI, ImmutableMap<RyaURI, Property>> properties;
private final int version;
private IRI smartUri = null;
/**
* To construct an instance of this class, use {@link Builder}.
* @param subject Identifies the thing that is being represented as an
* Entity.
* @param explicitTypeIds {@link Type}s that have been explicitly applied to
* the {@link Entity}.
* @param typeProperties All {@link Property}s that have been set for the
* Entity, grouped by Type ID.
* @param version The version of this Entity. This value is used by the
* {@link EntityStorage} to prevent stale updates.
* @param smartUri the Smart {@link IRI} representation of this
* {@link Entity}.
*/
private Entity(
final RyaURI subject,
final ImmutableList<RyaURI> explicitTypeIds,
final ImmutableMap<RyaURI, ImmutableMap<RyaURI, Property>> typeProperties,
final int version,
final IRI smartUri) {
this.subject = requireNonNull(subject);
this.explicitTypeIds = requireNonNull(explicitTypeIds);
properties = requireNonNull(typeProperties);
this.version = version;
if (smartUri != null) {
this.smartUri = smartUri;
} else {
// if Smart URI isn't provided create it from the given properties
try {
this.smartUri = SmartUriAdapter.serializeUriEntity(this);
} catch (final SmartUriException e) {
log.error("Unable to create a Smart URI for the entity", e);
}
}
}
/**
* To construct an instance of this class, use {@link Builder}.
* @param subject Identifies the thing that is being represented as an
* Entity.
* @param explicitTypeIds {@link Type}s that have been explicitly applied to
* the {@link Entity}.
* @param typeProperties All {@link Property}s that have been set for the
* Entity, grouped by Type ID.
* @param version The version of this Entity. This value is used by the
* {@link EntityStorage} to prevent stale updates.
*/
private Entity(
final RyaURI subject,
final ImmutableList<RyaURI> explicitTypeIds,
final ImmutableMap<RyaURI, ImmutableMap<RyaURI, Property>> typeProperties,
final int version) {
this(subject, explicitTypeIds, typeProperties, version, null);
}
/**
* @return Identifies the thing that is being represented as an Entity.
*/
public RyaURI getSubject() {
return subject;
}
/**
* @return {@link Type}s that have been explicitly applied to the {@link Entity}.
*/
public ImmutableList<RyaURI> getExplicitTypeIds() {
return explicitTypeIds;
}
/**
* @return All {@link Property}s that have been set for the Entity, grouped by Type ID.
*/
public ImmutableMap<RyaURI, ImmutableMap<RyaURI, Property>> getProperties() {
return properties;
}
/**
* @return The version of this Entity. This value is used by the {@link EntityStorage}
* to prevent stale updates.
*/
public int getVersion() {
return version;
}
/**
* @return the Smart {@link IRI} representation of this {@link Entity}.
*/
public IRI getSmartUri() {
return smartUri;
}
/**
* Does a lookup to see if the {@link Entity} contains the specified
* property for the specified type.
* @param typeRyaUri the type {@link RyaURI}. (not {@code null})
* @param propertyRyaUri the property {@link RyaURI}. (not {@code null})
* @return the {@link Property} or an empty {@link Optional} if it could not
* be found in the {@link Entity}.
*/
public Optional<Property> lookupTypeProperty(final Type type, final RyaURI propertyRyaUri) {
requireNonNull(type);
return lookupTypeProperty(type.getId(), propertyRyaUri);
}
/**
* Does a lookup to see if the {@link Entity} contains the specified
* property for the specified type.
* @param typeRyaUri the type {@link RyaURI}. (not {@code null})
* @param propertyRyaUri the property {@link RyaURI}. (not {@code null})
* @return the {@link Property} or an empty {@link Optional} if it could not
* be found in the {@link Entity}.
*/
public Optional<Property> lookupTypeProperty(final RyaURI typeRyaUri, final RyaURI propertyRyaUri) {
requireNonNull(typeRyaUri);
requireNonNull(propertyRyaUri);
final ImmutableMap<RyaURI, Property> typePropertyMap = properties.get(typeRyaUri);
Optional<Property> property = Optional.empty();
if (typePropertyMap != null) {
property = Optional.ofNullable(typePropertyMap.get(propertyRyaUri));
}
return property;
}
@Override
public int hashCode() {
return Objects.hash(subject, explicitTypeIds, properties, version);
}
@Override
public boolean equals(final Object o) {
if(this == o) {
return true;
}
if(o instanceof Entity) {
final Entity entity = (Entity) o;
return Objects.equals(subject, entity.subject) &&
Objects.equals(explicitTypeIds, entity.explicitTypeIds) &&
Objects.equals(properties, entity.properties) &&
version == entity.version;
}
return false;
}
/**
* Builds an {@link TypedEntity} using this object's values for the specified {@link Type}.
*
* @param typeId - The ID of the Type the TypedEntity will be for. (not null)
* @return A TypedEntity using this object's values if any properties for the Type
* are present or if the Type was explicitly set. Otherwise an empty {@link Optional}.
*/
public Optional<TypedEntity> makeTypedEntity(final RyaURI typeId) {
requireNonNull(typeId);
final boolean explicitlyHasType = explicitTypeIds.contains(typeId);
final boolean hasTypesProperties = properties.containsKey(typeId);
// The case where the MongoEntity can be represented as the typeId's Type.
if(explicitlyHasType || hasTypesProperties) {
// Set required fields.
final TypedEntity.Builder builder = TypedEntity.builder()
.setId( subject )
.setTypeId( typeId )
.setExplicitelyTyped( explicitTypeIds.contains(typeId) );
// Set Type's properties if present.
if(properties.containsKey(typeId)) {
properties.get(typeId).forEach( (propertyName, property) -> builder.setProperty(property));
}
return Optional.of( builder.build() );
}
// This MongoEntity can not be represented by the typeId's Type.
return Optional.empty();
}
/**
* @return An empty instance of {@link Builder}.
*/
public static Builder builder() {
return new Builder();
}
/**
* Create a {@link Builder} initialized with an {@link Entity}'s values.
*
* @param entity - The Entity the builder will be based on. (not null)
* @return A {@link Builder} loaded with {@code entity}'s values.
*/
public static Builder builder(final Entity entity) {
return new Builder(entity);
}
/**
* Builds instances of {@link Entity}.
*/
@DefaultAnnotation(NonNull.class)
public static class Builder {
private RyaURI subject = null;
private final List<RyaURI> explicitTypes = new ArrayList<>();
private final Map<RyaURI, Map<RyaURI, Property>> properties = new HashMap<>();
private IRI smartUri = null;
private int version = 0;
/**
* Constructs an empty instance of {@link Builder}.
*/
public Builder() { }
/**
* Constructs an instance of {@link Builder}.
*
* @param entity - The Entity the builder will be based on. (not null)
*/
public Builder(final Entity entity) {
requireNonNull(entity);
subject = entity.getSubject();
explicitTypes.addAll( entity.getExplicitTypeIds() );
for(final Entry<RyaURI, ImmutableMap<RyaURI, Property>> entry : entity.getProperties().entrySet()) {
properties.put(entry.getKey(), Maps.newHashMap(entry.getValue()));
}
version = entity.getVersion();
smartUri = entity.getSmartUri();
}
/**
* @param subject - Identifies the {@link TypedEntity}.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder setSubject(@Nullable final RyaURI subject) {
this.subject = subject;
return this;
}
/**
* @param typeId - A {@link Type} that has been explicity set for the {@link TypedEntity}.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder setExplicitType(@Nullable final RyaURI typeId) {
if(typeId != null) {
explicitTypes.add(typeId);
}
return this;
}
/**
* Removed a Type ID from the set of explicit Type IDs.
*
* @param typeId - The Type ID to remove from the set of explicit types.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder unsetExplicitType(@Nullable final RyaURI typeId) {
if(typeId != null) {
explicitTypes.remove(typeId);
}
return this;
}
/**
* Adds a {@link Property} for a specific {@link Type} of {@link TypedEntity}.
*
* @param typeId - The Type the Property is for.
* @param property - The Property values to add.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder setProperty(@Nullable final RyaURI typeId, @Nullable final Property property) {
if(typeId != null && property != null) {
if(!properties.containsKey(typeId)) {
properties.put(typeId, new HashMap<>());
}
properties.get(typeId).put(property.getName(), property);
}
return this;
}
/**
* Removes a {@link Property} for a specific {@link Type} of {@link TypedEntity}.
*
* @param typeId - The Type the Property will be removed from.
* @param propertyName - The name of the Property to remove.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder unsetProperty(@Nullable final RyaURI typeId, @Nullable final RyaURI propertyName) {
if(typeId != null && propertyName != null) {
if(properties.containsKey(typeId)) {
final Map<RyaURI, Property> typedProperties = properties.get(typeId);
if(typedProperties.containsKey(propertyName)) {
typedProperties.remove(propertyName);
}
}
}
return this;
}
/**
* @param smartUri - the Smart {@link IRI} representation of this
* {@link Entity}.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder setSmartUri(final IRI smartUri) {
this.smartUri = smartUri;
return this;
}
/**
* Indicates that the builder should rebuild the Smart URI. This should
* be used when properties or anything else in the {@link Entity} has
* changed.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder rebuildSmartUri() {
setSmartUri(null);
return this;
}
/**
* @param version - The version of this Entity. This value is used by the
* {@link EntityStorage} to prevent stale updates.
* @return This {@link Builder} so that method invocations may be chained.
*/
public Builder setVersion(final int version) {
this.version = version;
return this;
}
/**
* @return Builds an instance of {@link Entity} using this builder's values.
*/
public Entity build() {
final ImmutableMap.Builder<RyaURI, ImmutableMap<RyaURI, Property>> propertiesBuilder = ImmutableMap.builder();
for(final Entry<RyaURI, Map<RyaURI, Property>> entry : properties.entrySet()) {
propertiesBuilder.put(entry.getKey(), ImmutableMap.copyOf( entry.getValue() ));
}
return new Entity(subject,
ImmutableList.copyOf( explicitTypes ),
propertiesBuilder.build(),
version,
smartUri);
}
}
}