/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.librecms.assets;
import com.arsdigita.kernel.KernelConfig;
import org.libreccm.configuration.ConfigurationManager;
import org.libreccm.core.UnexpectedErrorException;
import org.libreccm.l10n.LocalizedString;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.PermissionChecker;
import org.libreccm.security.RequiresPrivilege;
import org.librecms.contentsection.Asset;
import org.librecms.contentsection.AssetRepository;
import org.librecms.contentsection.privileges.AssetPrivileges;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
/**
* Manages the language variants of an asset.
*
* @author Jens Pelzetter
*/
@RequestScoped
public class AssetL10NManager {
@Inject
private ConfigurationManager confManager;
@Inject
private AssetRepository assetRepo;
@Inject
private PermissionChecker permissionChecker;
private Locale defaultLocale;
private List supportedLocales;
@PostConstruct
private void init() {
final KernelConfig kernelConfig = confManager.findConfiguration(
KernelConfig.class);
defaultLocale = kernelConfig.getDefaultLocale();
supportedLocales = kernelConfig.getSupportedLanguages()
.stream()
.map(language -> new Locale(language))
.collect(Collectors.toList());
}
private List findLocalizedStringProperties(
final Asset asset) {
try {
final PropertyDescriptor[] properties = Introspector
.getBeanInfo(asset.getClass())
.getPropertyDescriptors();
return Arrays.stream(properties)
.filter(property -> {
return property
.getPropertyType()
.isAssignableFrom(LocalizedString.class);
})
.collect(Collectors.toList());
} catch (IntrospectionException ex) {
throw new UnexpectedErrorException(ex);
}
}
private LocalizedString readLocalizedString(final Asset asset,
final Method readMethod) {
try {
return (LocalizedString) readMethod.invoke(asset);
} catch (IllegalAccessException
| IllegalArgumentException
| InvocationTargetException ex) {
throw new UnexpectedErrorException(ex);
}
}
private Set collectLanguages(final Asset asset) {
final Set locales = new HashSet<>();
findLocalizedStringProperties(asset)
.stream()
.map(property -> property.getReadMethod())
.map(readMethod -> readLocalizedString(asset, readMethod))
.forEach(str -> locales.addAll(str.getAvailableLocales()));
return locales;
}
/**
* Helper method for reading methods in this class for verifying that the
* current user is permitted to read the item.
*
* @param asset The asset for which the read permission is verified
*/
private void checkReadPermission(final Asset asset) {
final String requiredPrivilege = AssetPrivileges.VIEW;
permissionChecker.checkPermission(requiredPrivilege, asset);
}
/**
* Retrieves all languages in which an asset is available.
*
* @param asset The asset.
*
* @return An (unmodifiable) {@link Set} containing all languages in which
* the asset is available.
*/
@Transactional(Transactional.TxType.REQUIRED)
public Set availableLocales(final Asset asset) {
checkReadPermission(asset);
return Collections.unmodifiableSet(collectLanguages(asset));
}
/**
* Checks if an asset has data for particular language.
*
* @param asset The asset to check.
* @param locale The locale to check for.
*
* @return {@link true} if the asset has data for the language, {@code false}
* if not.
*/
@AuthorizationRequired
@Transactional(Transactional.TxType.REQUIRED)
public boolean hasLanguage(
@RequiresPrivilege(AssetPrivileges.VIEW)
final Asset asset,
final Locale locale) {
Objects.requireNonNull(asset,
"Can't check if asset null has a specific locale.");
Objects.requireNonNull(locale, "Can't check for locale null.");
checkReadPermission(asset);
return collectLanguages(asset).contains(locale);
}
/**
* Returns a {@link Set} containing the locales for which the asset does not
* yet have a variant.
*
* @param asset The asset.
*
* @return A {@link Set} with the locales for which the asset does not have a
* variant.
*/
@Transactional(Transactional.TxType.REQUIRED)
public Set creatableLocales(final Asset asset) {
checkReadPermission(asset);
return supportedLocales.stream()
.filter(locale -> !hasLanguage(asset, locale))
.collect(Collectors.toSet());
}
/**
* Adds a language to an asset. The method will retrieve all fields of the
* type {@link LocalizedString} from the asset and add a new entry for the
* provided locale by coping the value for the default language configured
* in {@link KernelConfig}. If a field does not have an entry for the
* default language the first value found is used.
*
* @param asset The asset to which the language variant is added.
* @param locale The locale of the language to add.
*/
@AuthorizationRequired
@Transactional(Transactional.TxType.REQUIRED)
public void addLanguage(
@RequiresPrivilege(AssetPrivileges.EDIT)
final Asset asset,
final Locale locale) {
Objects.requireNonNull(asset, "Can't add language to asset null.");
Objects.requireNonNull(asset, "Cant't add language null to an asset.");
findLocalizedStringProperties(asset)
.forEach(property -> addLanguage(asset, locale, property));
assetRepo.save(asset);
}
private void addLanguage(final Asset asset,
final Locale locale,
final PropertyDescriptor property) {
final Method readMethod = property.getReadMethod();
final LocalizedString localizedStr = readLocalizedString(asset,
readMethod);
addLanguage(localizedStr, locale);
}
private void addLanguage(final LocalizedString localizedString,
final Locale locale) {
if (localizedString.hasValue(locale)) {
//Nothing to do
return;
}
final String value;
if (localizedString.hasValue(defaultLocale)) {
value = localizedString.getValue(defaultLocale);
} else {
value = findValue(localizedString);
}
localizedString.addValue(locale, value);
}
private String findValue(final LocalizedString localizedStr) {
final Optional locale = supportedLocales
.stream()
.filter(current -> localizedStr.hasValue(current))
.findAny();
if (locale.isPresent()) {
return localizedStr.getValue(locale.get());
} else {
return "Lorem ipsum";
}
}
/**
* Removes a language variant from an asset. This method will retrieve all
* fields of the type {@link LocalizedString} from the asset and remove the
* entry for the provided locale if the field has an entry for that locale.
*
* @param asset
* @param locale
*/
@AuthorizationRequired
@Transactional(Transactional.TxType.REQUIRED)
public void removeLanguage(
@RequiresPrivilege(AssetPrivileges.EDIT)
final Asset asset,
final Locale locale) {
Objects.requireNonNull(asset, "Can't remove language to asset null.");
Objects
.requireNonNull(asset, "Cant't remove language null to an asset.");
findLocalizedStringProperties(asset)
.forEach(property -> removeLanguage(asset, locale, property));
assetRepo.save(asset);
}
private void removeLanguage(final Asset asset,
final Locale locale,
final PropertyDescriptor property) {
final Method readMethod = property.getReadMethod();
final LocalizedString localizedStr = readLocalizedString(asset,
readMethod);
if (localizedStr.hasValue(locale)) {
localizedStr.removeValue(locale);
}
}
/**
* This method normalises the values of the fields of type
* {@link LocalizedString} of an asset. The method will first retrieve all
* fields of the type {@link LocalizedString} from the asset and than build
* a set with all locales provided by any of the fields. After that the
* method will iterate over all {@link LocalizedString} fields and check if
* the {@link LocalizedString} has an entry for every language in the set.
* If not an entry for the language is added.
*
* @param asset The asset to normalise.
*/
@AuthorizationRequired
@Transactional(Transactional.TxType.REQUIRED)
public void normalizeLanguages(
@RequiresPrivilege(AssetPrivileges.EDIT)
final Asset asset) {
Objects.requireNonNull(asset, "Can't normalise asset null");
final Set languages = collectLanguages(asset);
findLocalizedStringProperties(asset)
.stream()
.map(property -> property.getReadMethod())
.map(readMethod -> readLocalizedString(asset, readMethod))
.forEach(str -> normalize(str, languages));
}
private void normalize(final LocalizedString localizedString,
final Set languages) {
final List missingLangs = languages.stream()
.filter(lang -> !localizedString.hasValue(lang))
.collect(Collectors.toList());
if (!missingLangs.isEmpty()) {
missingLangs.stream()
.forEach(lang -> addLanguage(localizedString, lang));
}
}
}