/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.metadata.dublincore;
import static com.entwinemedia.fn.Stream.$;
import static org.opencastproject.util.EqualsUtil.eq;
import static org.opencastproject.util.data.Monadics.mlist;
import org.opencastproject.mediapackage.EName;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.XMLCatalogImpl;
import org.opencastproject.metadata.api.MetadataCatalog;
import org.opencastproject.util.RequireUtil;
import org.opencastproject.util.XmlNamespaceContext;
import org.opencastproject.util.data.Function;
import org.opencastproject.util.data.Function2;
import com.entwinemedia.fn.Fns;
import com.entwinemedia.fn.data.ImmutableSetWrapper;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.Transformer;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
/**
* Catalog for DublinCore structured metadata to be serialized as XML.
* <p>
* Attention: Encoding schemes are not preserved! See http://opencast.jira.com/browse/MH-8759
*/
@ParametersAreNonnullByDefault
public class DublinCoreCatalog extends XMLCatalogImpl implements DublinCore, MetadataCatalog, Cloneable {
private static final long serialVersionUID = -4568663918115847488L;
/** A flavor that matches any dublin core element */
public static final MediaPackageElementFlavor ANY_DUBLINCORE = MediaPackageElementFlavor.parseFlavor("dublincore/*");
private EName rootTag;
/** Create a new catalog. */
DublinCoreCatalog() {
}
public void setRootTag(EName rootTag) {
this.rootTag = rootTag;
}
@Nullable
public EName getRootTag() {
return rootTag;
}
public void addBindings(XmlNamespaceContext ctx) {
bindings = this.bindings.add(ctx);
}
@Override
public String toString() {
return "DublinCore" + (getIdentifier() != null ? "(" + getIdentifier() + ")" : "");
}
@Override
@SuppressWarnings("unchecked")
public List<String> get(EName property, final String language) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(language, "language");
if (LANGUAGE_ANY.equals(language)) {
return (List<String>) CollectionUtils.collect(getValuesAsList(property), new Transformer() {
@Override
public Object transform(Object o) {
return ((CatalogEntry) o).getValue();
}
});
} else {
final List<String> values = new ArrayList<String>();
final boolean langUndef = LANGUAGE_UNDEFINED.equals(language);
CollectionUtils.forAllDo(getValuesAsList(property), new Closure() {
@Override
public void execute(Object o) {
CatalogEntry c = (CatalogEntry) o;
String lang = c.getAttribute(XML_LANG_ATTR);
if ((langUndef && lang == null) || (language.equals(lang)))
values.add(c.getValue());
}
});
return values;
}
}
@Override
public List<DublinCoreValue> get(EName property) {
RequireUtil.notNull(property, "property");
return mlist(getValuesAsList(property)).map(toDublinCoreValue).value();
}
private DublinCoreValue toDublinCoreValue(CatalogEntry e) {
final String langRaw = e.getAttribute(XML_LANG_ATTR);
final String lang = langRaw != null ? langRaw : LANGUAGE_UNDEFINED;
final String typeRaw = e.getAttribute(XSI_TYPE_ATTR);
if (typeRaw != null) {
return DublinCoreValue.mk(e.getValue(), lang, toEName(typeRaw));
} else {
return DublinCoreValue.mk(e.getValue(), lang);
}
}
private final Function<CatalogEntry, DublinCoreValue> toDublinCoreValue = new Function<CatalogEntry, DublinCoreValue>() {
@Override
public DublinCoreValue apply(CatalogEntry e) {
return toDublinCoreValue(e);
}
};
@Override
public Map<EName, List<DublinCoreValue>> getValues() {
return mlist(data.values().iterator())
.foldl(new HashMap<EName, List<DublinCoreValue>>(),
new Function2<HashMap<EName, List<DublinCoreValue>>, List<CatalogEntry>, HashMap<EName, List<DublinCoreValue>>>() {
@Override
public HashMap<EName, List<DublinCoreValue>> apply(HashMap<EName, List<DublinCoreValue>> map,
List<CatalogEntry> entries) {
if (entries.size() > 0) {
final EName property = entries.get(0).getEName();
map.put(property, mlist(entries).map(toDublinCoreValue).value());
}
return map;
}
});
}
@Override public List<DublinCoreValue> getValuesFlat() {
return $(data.values()).bind(Fns.<List<CatalogEntry>>id()).map(toDublinCoreValue.toFn()).toList();
}
@Override
@Nullable
public String getFirst(EName property, String language) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(language, "language");
final CatalogEntry f = getFirstCatalogEntry(property, language);
return f != null ? f.getValue() : null;
}
@Override
public String getFirst(EName property) {
RequireUtil.notNull(property, "property");
final CatalogEntry f = getFirstCatalogEntry(property, LANGUAGE_ANY);
return f != null ? f.getValue() : null;
}
@Override
public DublinCoreValue getFirstVal(EName property) {
final CatalogEntry f = getFirstCatalogEntry(property, LANGUAGE_ANY);
return f != null ? toDublinCoreValue(f) : null;
}
private CatalogEntry getFirstCatalogEntry(EName property, String language) {
CatalogEntry entry = null;
switch (language) {
case LANGUAGE_UNDEFINED:
entry = getFirstLocalizedValue(property, null);
break;
case LANGUAGE_ANY:
for (CatalogEntry value : getValuesAsList(property)) {
entry = value;
// Prefer values without language information
if (!value.hasAttribute(XML_LANG_ATTR))
break;
}
break;
default:
entry = getFirstLocalizedValue(property, language);
break;
}
return entry;
}
@Override
public String getAsText(EName property, String language, String delimiter) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(language, "language");
RequireUtil.notNull(delimiter, "delimiter");
final List<CatalogEntry> values;
switch (language) {
case LANGUAGE_UNDEFINED:
values = getLocalizedValuesAsList(property, null);
break;
case LANGUAGE_ANY:
values = getValuesAsList(property);
break;
default:
values = getLocalizedValuesAsList(property, language);
break;
}
return values.size() > 0 ? $(values).mkString(delimiter) : null;
}
@Override
public Set<String> getLanguages(EName property) {
RequireUtil.notNull(property, "property");
Set<String> languages = new HashSet<String>();
for (CatalogEntry entry : getValuesAsList(property)) {
String language = entry.getAttribute(XML_LANG_ATTR);
if (language != null)
languages.add(language);
else
languages.add(LANGUAGE_UNDEFINED);
}
return languages;
}
@Override
public boolean hasMultipleValues(EName property, String language) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(language, "language");
return hasMultiplePropertyValues(property, language);
}
@Override
public boolean hasMultipleValues(EName property) {
RequireUtil.notNull(property, "property");
return hasMultiplePropertyValues(property, LANGUAGE_ANY);
}
private boolean hasMultiplePropertyValues(EName property, String language) {
if (LANGUAGE_ANY.equals(language)) {
return getValuesAsList(property).size() > 1;
} else {
int counter = 0;
for (CatalogEntry entry : getValuesAsList(property)) {
if (equalLanguage(language, entry.getAttribute(XML_LANG_ATTR)))
counter++;
if (counter > 1)
return true;
}
return false;
}
}
@Override
public boolean hasValue(EName property, String language) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(language, "language");
return hasPropertyValue(property, language);
}
@Override
public boolean hasValue(EName property) {
RequireUtil.notNull(property, "property");
return hasPropertyValue(property, LANGUAGE_ANY);
}
private boolean hasPropertyValue(EName property, final String language) {
if (LANGUAGE_ANY.equals(language)) {
return getValuesAsList(property).size() > 0;
} else {
return CollectionUtils.find(getValuesAsList(property), new Predicate() {
@Override
public boolean evaluate(Object o) {
return equalLanguage(((CatalogEntry) o).getAttribute(XML_LANG_ATTR), language);
}
}) != null;
}
}
@Override
public void set(EName property, @Nullable String value, String language) {
RequireUtil.notNull(property, "property");
if (language == null || LANGUAGE_ANY.equals(language))
throw new IllegalArgumentException("Language code may not be null or LANGUAGE_ANY");
setValue(property, value, language, null);
}
@Override
public void set(EName property, String value) {
RequireUtil.notNull(property, "property");
setValue(property, value, LANGUAGE_UNDEFINED, null);
}
@Override
public void set(EName property, @Nullable DublinCoreValue value) {
RequireUtil.notNull(property, "property");
if (value != null) {
setValue(property, value.getValue(), value.getLanguage(), value.getEncodingScheme().orNull());
} else {
removeValue(property, LANGUAGE_ANY);
}
}
@Override
public void set(EName property, List<DublinCoreValue> values) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(values, "values");
removeValue(property, LANGUAGE_ANY);
for (DublinCoreValue v : values) {
add(property, v);
}
}
private void setValue(EName property, @Nullable String value, String language, @Nullable EName encodingScheme) {
if (value == null) {
// No value, remove the whole element
removeValue(property, language);
} else {
String lang = !LANGUAGE_UNDEFINED.equals(language) ? language : null;
removeLocalizedValues(property, lang);
add(property, value, language, encodingScheme);
}
}
@Override
public void add(EName property, String value) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(value, "value");
add(property, value, LANGUAGE_UNDEFINED, null);
}
@Override
public void add(EName property, String value, String language) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(value, "value");
if (language == null || LANGUAGE_ANY.equals(language))
throw new IllegalArgumentException("Language code may not be null or LANGUAGE_ANY");
add(property, value, language, null);
}
@Override
public void add(EName property, DublinCoreValue value) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(value, "value");
add(property, value.getValue(), value.getLanguage(), value.getEncodingScheme().orNull());
}
void add(EName property, String value, String language, @Nullable EName encodingScheme) {
if (LANGUAGE_UNDEFINED.equals(language)) {
if (encodingScheme == null) {
addElement(property, value);
} else {
addTypedElement(property, value, encodingScheme);
}
} else {
// Language defined
if (encodingScheme == null) {
addLocalizedElement(property, value, language);
} else {
addTypedLocalizedElement(property, value, language, encodingScheme);
}
}
}
@Override
public void remove(EName property, String language) {
RequireUtil.notNull(property, "property");
RequireUtil.notNull(language, "language");
removeValue(property, language);
}
@Override
public void remove(EName property) {
RequireUtil.notNull(property, "property");
removeValue(property, LANGUAGE_ANY);
}
private void removeValue(EName property, String language) {
switch (language) {
case LANGUAGE_ANY:
removeElement(property);
break;
case LANGUAGE_UNDEFINED:
removeLocalizedValues(property, null);
break;
default:
removeLocalizedValues(property, language);
break;
}
}
@Override
public void clear() {
super.clear();
}
@Override
public Object clone() {
DublinCoreCatalog clone = new DublinCoreCatalog();
clone.setIdentifier(getIdentifier());
clone.setFlavor(getFlavor());
clone.setSize(getSize());
clone.setChecksum(getChecksum());
clone.bindings = bindings; // safe, since XmlNamespaceContext is immutable
clone.rootTag = rootTag;
for (Map.Entry<EName, List<CatalogEntry>> entry : data.entrySet()) {
EName elmName = entry.getKey();
EName elmNameCopy = new EName(elmName.getNamespaceURI(), elmName.getLocalName());
List<CatalogEntry> elmsCopy = new ArrayList<CatalogEntry>();
for (CatalogEntry catalogEntry : entry.getValue()) {
elmsCopy.add(new CatalogEntry(catalogEntry.getEName(), catalogEntry.getValue(), catalogEntry.getAttributes()));
}
clone.data.put(elmNameCopy, elmsCopy);
}
return clone;
}
@Override
public Set<EName> getProperties() {
return new ImmutableSetWrapper<>(data.keySet());
}
boolean equalLanguage(String a, String b) {
return (a == null && eq(b, LANGUAGE_UNDEFINED)) || (b == null && eq(a, LANGUAGE_UNDEFINED)) || eq(a, LANGUAGE_ANY)
|| eq(b, LANGUAGE_ANY) || (a != null && eq(a, b));
}
// make public
@Override public EName toEName(String qName) {
return super.toEName(qName);
}
// make public
@Nonnull @Override public String toQName(EName eName) {
return super.toQName(eName);
}
// make public
@Override public void addElement(EName element, String value, Attributes attributes) {
super.addElement(element, value, attributes);
}
// make public
@Override public CatalogEntry[] getValues(EName element) {
return super.getValues(element);
}
// make public
@Override public List<CatalogEntry> getEntriesSorted() {
return super.getEntriesSorted();
}
/**
* Saves the dublin core metadata container to a dom.
*
* @throws ParserConfigurationException
* if the xml parser environment is not correctly configured
* @throws TransformerException
* if serialization of the metadata document fails
* @throws IOException
* if an error with catalog serialization occurs
*/
@Override
public Document toXml() throws ParserConfigurationException, TransformerException, IOException {
return DublinCoreXmlFormat.writeDocument(this);
}
@Override
public String toJson() throws IOException {
return DublinCoreJsonFormat.writeJsonObject(this).toJSONString();
}
}