/**
* (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jabylon.rest.ui.wicket.xliff;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.wicket.model.IModel;
import org.jabylon.properties.ProjectVersion;
import org.jabylon.properties.Property;
import org.jabylon.properties.PropertyFileDescriptor;
import org.jabylon.properties.Resolvable;
import org.jabylon.properties.Review;
import org.jabylon.properties.xliff.PropertyWrapper;
import org.jabylon.properties.xliff.XliffResourceImpl;
import org.jabylon.resources.persistence.PropertyPersistenceService;
import org.jabylon.rest.ui.Activator;
import org.jabylon.rest.ui.model.PropertyPair;
import org.jabylon.rest.ui.wicket.panels.PropertyListMode;
/**
* Exports a project as an archive containing xliff files
*
* @author c.samulski (29.01.2016)
*/
public final class XliffDownloadHelper {
/**
* The {@link ProjectVersion} we are preparing the XLIFF conversion and download for.<br>
*/
private transient final ProjectVersion projectVersion;
/**
* The set {@link Map} of language tuples for which we will be retrieving and converting
* {@link Property}s.<br>
*/
private transient final Map<Language, Language> languageTuples;
/**
* The {@link PropertyListMode} a user may have selected, e.g. {@link PropertyListMode#ALL},
* {@link PropertyListMode#FUZZY}, or {@link PropertyListMode#MISSING}.<br>
*/
private transient final PropertyListMode filter;
/**
* The response {@link OutputStream} which the ZIP file will be written to.<br>
*/
private transient final ZipOutputStream zipOut;
/**
* The only Constructor of this class.<br>
* Note that the {@link Resolvable} passed here must be of type {@link ProjectVersion}.<br>
* Currently, aggregated XLIFF conversions/downloads are only allowed on {@link ProjectVersion}
* level.<br>
*/
public XliffDownloadHelper(IModel<Resolvable<?, ?>> projectVersion, Map<Language, Language> languageTuples,
PropertyListMode filter, OutputStream out) {
/**
* Create ZipStream to write ZIP file to the response OutputStream.<br>
*/
this.zipOut = new ZipOutputStream(new BufferedOutputStream(out));
/**
* Do the cast to ProjectVersion here.<br>
*/
this.projectVersion = ((ProjectVersion) projectVersion.getObject());
this.languageTuples = languageTuples;
this.filter = filter;
}
public PropertyPersistenceService getPersistenceService() {
return Activator.getDefault().getPersistenceService();
}
/**
* Basically Main() for this class. The only public method.<br>
* Calls "Properties to XLIFF Conversion" for every source/target language tuple, write to ZIP
* stream.<br>
*/
public final void handleXliffDownload() throws IOException {
try {
for (Map.Entry<Language, Language> tuple : languageTuples.entrySet()) {
convertAndZip(tuple);
}
zipOut.finish();
} catch (ExecutionException e) {
if(e.getCause() instanceof IOException)
throw (IOException)e.getCause();
throw new IOException("Failed to load property values",e);
} finally {
zipOut.close();
}
}
/**
* Retrieves {@link Property}s for the passed {@link Language} tuples, matches source and target
* language module files, passes them to
* {@link #writeZipEntry(PropertyFileDescriptor, PropertyFileDescriptor, String, PropertyListMode, ZipOutputStream)}
* for XLIFF conversion and the writing of a ZIP entry to the stream.<br>
* @throws ExecutionException
*/
private void convertAndZip(Map.Entry<Language, Language> tuple) throws IOException, ExecutionException {
Locale targetLocale = getLocale(tuple.getKey());
Locale sourceLocale = getLocale(tuple.getValue());
Map<String, PropertyFileDescriptor> targetDescriptors = loadDescriptorsForLocale(targetLocale);
Map<String, PropertyFileDescriptor> sourceDescriptors = loadDescriptorsForLocale(sourceLocale);
/* Iterate over target descriptors. */
for (Map.Entry<String, PropertyFileDescriptor> target : targetDescriptors.entrySet()) {
String templatePath = target.getKey();
PropertyFileDescriptor targetDescriptor = target.getValue();
PropertyFileDescriptor sourceDescriptor = sourceDescriptors.get(templatePath);
writeZipEntry(targetDescriptor, sourceDescriptor, targetLocale, sourceLocale);
}
}
/**
* Write a single XLIFF file as {@link ZipEntry} to our {@link ZipOutputStream}.<br>
* Conversion from source/target {@link PropertyFileDescriptor}s to XLIFF is done on the fly.<br>
* @throws ExecutionException
*/
private void writeZipEntry(PropertyFileDescriptor targetDescriptor, PropertyFileDescriptor sourceDescriptor,
Locale targetLocale, Locale sourceLocale) throws IOException, ExecutionException {
/*
* 1. Target language: Retrieve filtered list of properties.<br>
* 2. Source language: Retrieve filtered list based on filtered target language keys.<br>
*/
try {
PropertyWrapper target = new PropertyWrapper(targetLocale, filterTarget(targetDescriptor));
PropertyWrapper source = new PropertyWrapper(sourceLocale, filterSource(sourceDescriptor, target.getProperties().keySet()));
/*
* 3. Retrieve properly formatted filename. (module_srcLang_trgLang.xml) <br>
* 4. Create new ZipEntry.<br>
*/
String fileNameWithPath = getXliffFileName(sourceDescriptor, targetDescriptor);
ZipEntry entry = new ZipEntry(fileNameWithPath);
zipOut.putNextEntry(entry);
/*
* 5. Call Property to XLIFF converter (will write to stream).<br>
*/
new XliffResourceImpl().write(source, target, zipOut);
zipOut.closeEntry();
} catch (ExecutionException e) {
if(e.getCause() instanceof IOException)
throw (IOException)e.getCause();
throw new IOException("Failed to load property values",e);
}
}
/**
* Helper to reduce the Map of {@link Property}s retrieved from the passed
* {@link PropertyFileDescriptor} to match the passed keys.<br>
*
* (Could frankly be omitted for now, as we only get via keys later on anyway. (just for
* clarity).<br>
*
* 1. Load complete Map of source language properties.<br>
* 2. Filter them according to the keySet parameter and return the reduced map.<br>
* @throws ExecutionException
*/
private Map<String, Property> filterSource(PropertyFileDescriptor sourceDescriptor, Set<String> keySet) throws ExecutionException {
Map<String, Property> filtered = new LinkedHashMap<String, Property>(keySet.size(),1f);
Map<String, Property> sourceProperties = getPersistenceService().loadProperties(sourceDescriptor).asMap();
for (String key : keySet) {
Property value = sourceProperties.get(key);
filtered.put(key, value);
}
return filtered;
}
/**
* Filters the {@link Property}s retrieved via the passed {@link PropertyFileDescriptor}
* according to the passed {@link PropertyListMode}.<br>
*
* @return {@link Map} of filtered {@link Property}s.
* @throws ExecutionException
*/
private Map<String, Property> filterTarget(PropertyFileDescriptor descriptor) throws ExecutionException {
/*
* 1. Instantiate the return collection. Retain order.<br>
*/
Map<String, Property> filtered = new LinkedHashMap<String, Property>();
/*
* 2. Get master properties and this locale descriptor's reviews.<br>
*/
Map<String, List<Review>> reviews = reviewsAsMap(descriptor.getReviews());
PropertyFileDescriptor master = descriptor.getMaster();
List<Property> templateProperties = getPersistenceService().loadProperties(master).getProperties();
/*
* 3. Load translated properties.<br>
*/
Map<String, Property> translated = getPersistenceService().loadProperties(descriptor).asMap();
/*
* 4. Iterate over template properties, check if the given filter applies, add to our return
* collection if true.<br>
*/
for (Property property : templateProperties) {
Property translatedProperty = translated.get(property.getKey());
PropertyPair pair = new PropertyPair(property, translatedProperty, descriptor.getVariant(), descriptor.cdoID());
if (filter.apply(pair, reviews.get(pair.getKey()))) {
filtered.put(property.getKey(), translated.get(property.getKey()));
}
}
return filtered;
}
/**
* Helper method. Create Map with K=Template Name, V=Descriptor.<br>
*
* This associates a property descriptor with the path of its respective template for faster lookups.<br>
*/
private Map<String, PropertyFileDescriptor> loadDescriptorsForLocale(Locale locale) {
Map<String, PropertyFileDescriptor> ret = new LinkedHashMap<String, PropertyFileDescriptor>();
List<PropertyFileDescriptor> descriptors = null;
/*
* Load the List of descriptors available for this locale.<br>
* Note that a null locale implies that either no source language was selected, or the
* selected source language is the template language.<br>
*/
if (locale == null) {
descriptors = projectVersion.getTemplate().getDescriptors();
} else {
descriptors = projectVersion.getProjectLocale(locale).getDescriptors();
}
for (PropertyFileDescriptor descriptor : descriptors) {
ret.put(descriptor.isMaster() ? descriptor.getLocation().toString() : descriptor.getMaster().getLocation().toString(), descriptor);
}
return ret;
}
/**
* Helper method. Construct a filename for *this* particular XLIFF translation file.<br>
* (Not the fileName of the ZIP to be served later).<br>
* Format will be [moduleName]_[targetLangISO].properties.xml
*/
private static String getXliffFileName(PropertyFileDescriptor sourceDescriptor, PropertyFileDescriptor targetDescriptor) {
return targetDescriptor.getLocation().path() + ".xlf";
}
/**
* Helper method. Converts {@link List} to {@link Map} to be consumed by
* {@link PropertyListMode#apply(PropertyPair, java.util.Collection)}
*/
private static Map<String, List<Review>> reviewsAsMap(List<Review> reviews) {
Map<String, List<Review>> ret = new HashMap<String, List<Review>>();
for (Review r : reviews) {
if (!ret.containsKey(r.getKey())) {
ret.put(r.getKey(), new ArrayList<Review>());
}
ret.get(r.getKey()).add(r);
}
return ret;
}
/**
* Helper method. Return {@link Locale} from {@link Language} or null if {@link Language} is
* null.<br>
*/
private static Locale getLocale(Language language) {
return language == null ? null : language.getLocale();
}
}