/* * Copyright 2013 NGDATA nv * * Licensed 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.lilyproject.repository.api; import java.util.Collections; import java.util.Map; import java.util.Set; import org.joda.time.DateTime; import org.lilyproject.bytes.api.ByteArray; import org.lilyproject.util.ObjectUtils; /** * A set of free, typed key-value pairs. * * <p>Metadata objects can only be created using the {@link MetadataBuilder} and are immutable after creation.</p> * * <p>This metadata can be associated with record field values, see {@link Record#setMetadata(QName, Metadata)}.</p> */ public class Metadata implements Cloneable { private Map<String, Object> data; private Set<String> fieldsToDelete; protected Metadata(Map<String, Object> data, Set<String> fieldsToDelete) { this.data = Collections.unmodifiableMap(data); this.fieldsToDelete = Collections.unmodifiableSet(fieldsToDelete); } public String get(String key) { return data.get(key).toString(); } /** * Returns the metadata value without any type coercion. */ public Object getObject(String key) { return data.get(key); } public Integer getInt(String key, Integer defaultValue) { Object value = data.get(key); if (value == null) { return defaultValue; } else if (value instanceof Number) { return ((Number)value).intValue(); } else { return Integer.parseInt(value.toString()); } } public Long getLong(String key, Long defaultValue) { Object value = data.get(key); if (value == null) { return defaultValue; } else if (value instanceof Number) { return ((Number)value).longValue(); } else { return Long.parseLong(value.toString()); } } public Boolean getBoolean(String key, Boolean defaultValue) { Object value = data.get(key); if (value == null) { return defaultValue; } else if (value instanceof Boolean) { return (Boolean)value; } else { throw new IllegalStateException("Value is not a boolean for metadata field '" + key + "': " + value.getClass().getName()); } } public Float getFloat(String key, Float defaultValue) { Object value = data.get(key); if (value == null) { return defaultValue; } else if (value instanceof Number) { return ((Number)value).floatValue(); } else { return Float.parseFloat(value.toString()); } } public Double getDouble(String key, Double defaultValue) { Object value = data.get(key); if (value == null) { return defaultValue; } else if (value instanceof Number) { return ((Number)value).doubleValue(); } else { return Double.parseDouble(value.toString()); } } public ByteArray getBytes(String key) { Object value = data.get(key); if (value instanceof ByteArray) { return (ByteArray)value; } else { throw new IllegalStateException("Value is not a byte array for metadata field '" + key + "': " + value.getClass().getName()); } } public DateTime getDateTime(String key, DateTime defaultValue) { Object value = data.get(key); if (value == null) { return defaultValue; } else if (value instanceof DateTime) { return (DateTime)value; } else { throw new IllegalStateException("Value is not a DateTime for metadata field '" + key + "': " + value.getClass().getName()); } } public boolean contains(String key) { return data.containsKey(key); } /** * Checks there are no metadata fields and that the fields-to-delete is empty. * * <p>If you only want to know if there are no fields (ignoring fieldsToDelete), use * {@link #getMap()}.isEmtpy().</p> */ public boolean isEmpty() { return data.isEmpty() && fieldsToDelete.isEmpty(); } public Map<String, Object> getMap() { return data; } public Set<String> getFieldsToDelete() { return fieldsToDelete; } /** * Tests if this metadata will change the existing metadata on a field. * * <p>This is different from a normal equals, since it looks at the effect of the * {@link #getFieldsToDelete() fields to delete}, rather than comparing the lists * of fields to delete.</p> * * <p>This method can e.g. be used to set a metadata field only if there are already * metadata changes, or to figure out if a record changed compared to a previous state.</p> * * @param oldMetadata the current metadata on a field. This can be null, in which case this * method will always return true. */ public boolean updates(Metadata oldMetadata) { if (oldMetadata == null) { return true; } // Metadata has not changed if: // - all KV's in the new metadata are also in the old metadata // - any deletes in the new metadata refer to fields that didn't exist in the old metadata for (Map.Entry<String, Object> entry : this.getMap().entrySet()) { Object oldValue = oldMetadata.getObject(entry.getKey()); if (!ObjectUtils.safeEquals(oldValue, entry.getValue())) { return true; } } for (String key : this.getFieldsToDelete()) { if (oldMetadata.contains(key)) { return true; } } return false; } @Override public String toString() { return "Metadata{" + "data=" + data + ", fieldsToDelete=" + fieldsToDelete + '}'; } }