/** * (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 static org.jabylon.rest.ui.wicket.xliff.XliffUploadResultMessageKeys.INVALID_ARCHIVE; import static org.jabylon.rest.ui.wicket.xliff.XliffUploadResultMessageKeys.INVALID_FILENAME; import static org.jabylon.rest.ui.wicket.xliff.XliffUploadResultMessageKeys.NO_FILE_MATCH; import static org.jabylon.rest.ui.wicket.xliff.XliffUploadResultMessageKeys.PARSE_SAX; import static org.jabylon.rest.ui.wicket.xliff.XliffUploadResultMessageKeys.SUCCESS; import static org.jabylon.rest.ui.wicket.xliff.XliffUploadResultMessageKeys.UNPARSABLE; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; 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.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.zip.ZipEntry; import org.apache.wicket.model.IModel; import org.jabylon.properties.ProjectVersion; import org.jabylon.properties.PropertiesFactory; import org.jabylon.properties.Property; import org.jabylon.properties.PropertyFile; import org.jabylon.properties.PropertyFileDescriptor; import org.jabylon.properties.Resolvable; import org.jabylon.properties.xliff.PropertyWrapper; import org.jabylon.properties.xliff.XliffReader; import org.jabylon.properties.xliff.XliffZipInputStream; import org.jabylon.resources.persistence.PropertyPersistenceService; import org.jabylon.rest.ui.Activator; import org.jabylon.rest.ui.wicket.panels.PropertyListPanel; import org.jabylon.rest.ui.wicket.xliff.XliffUploadResult.Level; import org.xml.sax.SAXException; /** * Processes the upload of a ZIP archive containing multiple XLIFF documents.<br> * {@link #handleUpload()} is basically Main() and will return a {@link List} of * {@link XliffUploadResult}s which can be used for displaying appropriate per {@link ZipEntry} * status messages.<br> * * @author c.samulski (2016-02-09) */ public final class XliffUploadHelper { /** * The {@link ProjectVersion} we are are parsing the uploaded XLIFF file for.<br> */ private transient final ProjectVersion version; /** * The request {@link InputStream} we will be reading the ZIP archive (containing the XLIFF * translation files) from. We will *not* be closing this stream here.<br> */ private transient final XliffZipInputStream in; /** * Filled during {@link #readFromStream()} and {@link #handleImport(Entry)} processing, used to * notify UI about {@link ZipEntry}s which could not be parsed or matched successfully, * respectively.<br> * Initialize with default {@link Level#}s, maintaining insertion order.<br> */ private transient final Map<Level, List<XliffUploadResult>> uploadResult = new LinkedHashMap<>(); { uploadResult.put(Level.INFO, new ArrayList<XliffUploadResult>()); uploadResult.put(Level.WARNING, new ArrayList<XliffUploadResult>()); uploadResult.put(Level.ERROR, new ArrayList<XliffUploadResult>()); } /** * The only Constructor of this class.<br> * Note that the {@link Resolvable} passed here must be of type {@link ProjectVersion}.<br> * Currently, aggregated XLIFF uploads (and thus conversions into {@link Property}s) are only * allowed on {@link ProjectVersion} level.<br> */ public XliffUploadHelper(IModel<Resolvable<?, ?>> projectVersion, InputStream in) { this.in = new XliffZipInputStream(new BufferedInputStream(in)); this.version = (ProjectVersion) projectVersion.getObject(); } /** * Basically Main() for this class. Work order:<br> * * 1. Consume request {@link InputStream}, read the ZIP contents into {@link PropertyWrapper}s.<br> * * 2. Load matching {@link PropertyFileDescriptor}s.<br> * * 3. Merge retrieved {@link Property}s into existing {@link PropertyFileDescriptor}.<br> * * 4. Persist via {@link PropertyPersistenceService}.<br> * * @return a {@link Map} of file names for {@link ZipEntry}s which could not be parsed or * matched to existing {@link PropertyFileDescriptor}s.<br> */ public final Map<Level, List<XliffUploadResult>> handleUpload() throws IOException { try { Map<String, PropertyWrapper> wrappers = readFromStream(); /* Here be per XLIFF file processing. */ for (Map.Entry<String, PropertyWrapper> entry : wrappers.entrySet()) { handleImport(entry); } } catch (ExecutionException e) { if (e.getCause() instanceof IOException) { throw (IOException) e.getCause(); } throw new IOException("Failed to load property values", e); } finally { in.doClose(); } return uploadResult; } /** * Read files from ZIP, call {@link XliffReader} for each {@link ZipEntry}.<br> */ private Map<String, PropertyWrapper> readFromStream() throws IOException { Map<String, PropertyWrapper> ret = new HashMap<String, PropertyWrapper>(); ZipEntry entry = null; boolean processed = false; while ((entry = in.getNextEntry()) != null) { if (entry.isDirectory()) { continue; // we only pass files to the parser. } processed = true; // signal that we've read at least one file in this archive. String key = entry.getName(); try { ret.put(key, XliffReader.read(in, StandardCharsets.UTF_8.displayName())); } catch (IOException e) { addUploadResult(new XliffUploadResult(UNPARSABLE, Level.ERROR, key)); } catch (SAXException e) { addUploadResult(new XliffUploadResult(PARSE_SAX, Level.ERROR, key, e.getLocalizedMessage())); } } /* * If no file was processed, i.e. not a single ZipEntry was found, this is probably not a ZIP archive. Let the user know. */ if (!processed) { addUploadResult(new XliffUploadResult(INVALID_ARCHIVE, Level.ERROR, null)); } return ret; } /** * Handles the per {@link ZipEntry} import of new {@link Property}s.<br> */ private void handleImport(Entry<String, PropertyWrapper> entry) throws ExecutionException { String fileName = getFileName(entry.getKey()); /* * Validate FileName first. Return to caller if not conform (i.e. ends in .xlf). */ if (fileName == null) { addUploadResult(new XliffUploadResult(INVALID_FILENAME, Level.ERROR, entry.getKey())); return; } Locale locale = entry.getValue().getLocale(); Map<String, Property> properties = entry.getValue().getProperties(); /* * Retrieve PropertyFileDescriptors for this locale. */ List<PropertyFileDescriptor> descriptors = version.getProjectLocale(locale).getDescriptors(); /* * Find the corresponding descriptor, match by fileName. */ for (PropertyFileDescriptor descriptor : descriptors) { if (fileName.equals(descriptor.getLocation().path())) { int updated = merge(properties, descriptor); if (updated > 0) { addUploadResult(new XliffUploadResult(SUCCESS, Level.INFO, fileName, String.valueOf(updated))); } return; } } /* * This ZipEntry could not be matched to any existing PropertyFile. */ addUploadResult(new XliffUploadResult(NO_FILE_MATCH, Level.WARNING, fileName)); } /** * Add the {@link XliffUploadResult} to {@link #uploadResult}.<br> */ private void addUploadResult(XliffUploadResult result) { List<XliffUploadResult> resultList = uploadResult.get(result.getLevel()); if (resultList == null) { uploadResult.put(result.getLevel(), resultList = new ArrayList<>()); } resultList.add(result); } /** * @return null if the filename does not end with ".xlf". Caller has to handle that.<br> */ private String getFileName(String key) { if (key.indexOf(".xlf") == -1) { return null; } return key.split(".xlf")[0]; } /** * Merges a {@link Map} of {@link Property}s into the existing {@link PropertyFileDescriptor}.<br> * TODO: Possibly merge with persist logic in {@link PropertyListPanel}.<br> * * @return count of updated properties.<br> */ private static int merge(Map<String, Property> newProperties, PropertyFileDescriptor descriptor) throws ExecutionException { PropertyFile existingFile = getPersistenceService().loadProperties(descriptor); Map<String, Property> existingProperties = existingFile.asMap(); Map<String, Property> master = descriptor.getMaster().loadProperties().asMap(); int updateCount = 0; for (Property newProperty : newProperties.values()) { Property existingProperty = existingProperties.get(newProperty.getKey()); if (!master.containsKey(newProperty.getKey())) { continue; // we're not creating new K/V pairs, only updating. } if (!updatePropertyValue(newProperty, existingProperty)) { continue; // not setting null, but we allow empty (value deletion). } updateExistingProperty(existingFile, existingProperty, newProperty); updateCount++; // trigger persist. } if (updateCount > 0) { getPersistenceService().saveProperties(descriptor, existingFile); } return updateCount; } /** * If we make it here, we know that the {@link Property} exists in the master * {@link PropertyFileDescriptor}, but we don't know if a corresponding {@link Property} exists * for the existing {@link PropertyFile}. If it does not, we create a new {@link Property}.<br> * * Hence perform a quick check, and create a new {@link Property} which we add to this * {@link PropertyFileDescriptor} if it does not yet exist.<br> */ private static void updateExistingProperty(PropertyFile existingFile, Property existingProperty, Property newProperty) { if (existingProperty == null) { existingProperty = PropertiesFactory.eINSTANCE.createProperty(); existingProperty.setKey(newProperty.getKey()); existingFile.getProperties().add(existingProperty); } existingProperty.setValue(newProperty.getValue()); existingProperty.setComment(newProperty.getComment()); } /** * Decide if we need to update the old {@link Property} with the value of the new one.<br> */ private static boolean updatePropertyValue(Property newProperty, Property oldProperty) { String oldValue = oldProperty == null ? null : oldProperty.getValue(); String newValue = newProperty == null ? null : newProperty.getValue(); /* no translation value was sent. */ if (newProperty == null) { return false; } /* if new is empty, but old is not, we will want to update */ if (!hasValue(oldValue) && !hasValue(newValue)) { return false; } /* only update if values differ */ return !newValue.equals(oldValue); } private static boolean hasValue(String value) { return value != null && !"".equals(value); } private static PropertyPersistenceService getPersistenceService() { return Activator.getDefault().getPersistenceService(); } }