/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * 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 uk.q3c.krail.core.persist.clazz.i18n;
import com.google.inject.Inject;
import com.vaadin.data.Property;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.lang3.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.i18n.DescriptionKey;
import uk.q3c.krail.core.i18n.I18NKey;
import uk.q3c.krail.core.i18n.LabelKey;
import uk.q3c.krail.core.option.Option;
import uk.q3c.krail.core.option.OptionContext;
import uk.q3c.krail.core.option.OptionKey;
import uk.q3c.krail.core.persist.cache.i18n.PatternCacheKey;
import uk.q3c.krail.core.persist.common.i18n.PatternDao;
import uk.q3c.krail.core.persist.common.i18n.PatternWriteException;
import javax.annotation.Nonnull;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.Optional;
import java.util.ResourceBundle;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A {@link PatternDao} implementation used with {@link EnumResourceBundle} instances held within code. Writing back to source code is clearly not an option,
* but this implementation can be set up to write source code files to a defined external directory.
* <p>
* Created by David Sowerby on 27/07/15.
*/
public class DefaultClassPatternDao implements ClassPatternDao, OptionContext {
public static final String CONNECTION_URL = "Class based";
public static final OptionKey<String> optionPathToValues = new OptionKey<>("", DefaultClassPatternDao.class, LabelKey.Path, DescriptionKey.Path);
public static final OptionKey<Boolean> optionKeyUseKeyPath = new OptionKey<>(Boolean.TRUE, DefaultClassPatternDao.class, LabelKey.Use_Key_Path,
DescriptionKey
.Use_Key_Path);
private static Logger log = LoggerFactory.getLogger(DefaultClassPatternDao.class);
protected Class<? extends Annotation> source;
private ClassBundleControl control;
private Option option;
private File writeFile;
@Inject
protected DefaultClassPatternDao(ClassBundleControl control, Option option) {
super();
this.control = control;
this.option = option;
source = ClassPatternSource.class;
}
/**
* {@inheritDoc}
*/
@Override
public File getWriteFile() {
return writeFile;
}
/**
* {@inheritDoc}
*/
@Override
public void setWriteFile(@Nonnull File writeFile) {
checkNotNull(writeFile);
this.writeFile = writeFile;
}
/**
* {@inheritDoc}
*/
@SuppressFBWarnings("EXS_EXCEPTION_SOFTENING_NO_CHECKED")
@Override
public Object write(@Nonnull PatternCacheKey cacheKey, @Nonnull String value) {
checkNotNull(cacheKey);
checkNotNull(value);
if (writeFile == null) {
throw new PatternWriteException("Write file must be set");
}
if (!writeFile.exists()) {
throw new PatternWriteException("Write file must exist");
}
String indent = " ";
String indent2 = indent + indent;
StringBuilder buf = new StringBuilder(indent2);
buf.append("put(")
.append(cacheKey.getKeyAsEnum()
.name())
.append(", \"")
.append(value)
.append("\");\n");
CharsetEncoder encoder = Charset.forName("UTF-8")
.newEncoder();
encoder.onMalformedInput(CodingErrorAction.REPORT);
encoder.onUnmappableCharacter(CodingErrorAction.REPORT);
String output = buf.toString();
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(writeFile), encoder))) {
writer.write(output);
return output;
} catch (Exception e) {
throw new PatternWriteException("failed to write pattern", e);
}
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public Optional<String> deleteValue(@Nonnull PatternCacheKey cacheKey) {
throw new UnsupportedOperationException("Class based I18NPatterns cannot be deleted");
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public Optional<String> getValue(@Nonnull PatternCacheKey cacheKey) {
checkNotNull(cacheKey);
// source is used to qualify the Option
log.debug("getValue for cacheKey {}, source '{}', using control: {}", cacheKey, source, getControl().getClass()
.getSimpleName());
I18NKey key = cacheKey.getKey();
String expandedBaseName = expandFromKey(key);
try {
ResourceBundle bundle = ResourceBundle.getBundle(expandedBaseName, cacheKey.getActualLocale(), getControl());
return Optional.of(getValue(bundle, cacheKey.getKeyAsEnum()));
} catch (Exception e) {
log.warn("returning empty value, as getValue() returned exception {} with message '{}'", e, e.getMessage());
return Optional.empty();
}
}
protected String getValue(@Nonnull ResourceBundle bundle, @Nonnull Enum<?> key) {
EnumResourceBundle enumBundle = (EnumResourceBundle) bundle;
//noinspection unchecked
enumBundle.setKeyClass(key.getClass());
enumBundle.load();
//noinspection unchecked
return enumBundle.getValue(key);
}
/**
* Allows the setting of paths for location of class and property files. The bundle base name is taken from {@link
* I18NKey#bundleName()}.
* <p>
* {@link Option} entries determine how the bundle name is expanded. If {@link #optionKeyUseKeyPath} is true, the bundle name is
* appended to the package path of the {@code sampleKey}
* <p>
* If {@link #optionKeyUseKeyPath} is false, the bundle name is appended to {@link #optionPathToValues}
*
* @param sampleKey any key from the I18NKey class, to give access to bundleName()
* @return a path constructed from the {@code sampleKey} and {@link Option} values
*/
protected String expandFromKey(@Nonnull I18NKey sampleKey) {
checkNotNull(sampleKey);
String baseName = sampleKey.bundleName();
String packageName;
//use source to qualify the options, so they get their own, and not the base class
if (option.get(optionKeyUseKeyPath.qualifiedWith(getSourceString()))) {
packageName = ClassUtils.getPackageCanonicalName(sampleKey.getClass());
} else {
String pathOptionValue = option.get(optionPathToValues.qualifiedWith(getSourceString()));
if (pathOptionValue.isEmpty() || ".".equals(pathOptionValue)) {
packageName = ClassUtils.getPackageCanonicalName(sampleKey.getClass());
} else {
packageName = pathOptionValue;
}
}
return packageName.isEmpty() ? baseName : packageName + '.' + baseName;
}
public String getSourceString() {
return source.getSimpleName();
}
public ResourceBundle.Control getControl() {
return control;
}
/**
* Returns {@link DefaultClassPatternDao#CONNECTION_URL} as a connection url
*
* @return {@link DefaultClassPatternDao#CONNECTION_URL} as a connection url
*/
@Override
public String connectionUrl() {
return DefaultClassPatternDao.CONNECTION_URL;
}
/**
* {@inheritDoc}
*/
@Override
public long count() {
throw new UnsupportedOperationException("count is not available for class based patterns");
}
@Override
public void optionValueChanged(Property.ValueChangeEvent event) {
//does nothing, option values are called as required
}
/**
* Returns the {@link Option} instance being used by this context
*
* @return the {@link Option} instance being used by this context
*/
@Nonnull
@Override
public Option getOption() {
return option;
}
}