001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     thibaud
016 */
017package org.nuxeo.diff.pictures;
018
019import java.io.BufferedReader;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.io.Serializable;
025import java.util.Calendar;
026import java.util.HashMap;
027import java.util.Map;
028import java.util.Map.Entry;
029
030import org.apache.commons.lang.StringEscapeUtils;
031import org.apache.commons.lang.StringUtils;
032import org.nuxeo.ecm.core.api.Blob;
033import org.nuxeo.ecm.core.api.Blobs;
034import org.nuxeo.ecm.core.api.CloseableFile;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.NuxeoException;
037import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters;
038import org.nuxeo.ecm.platform.commandline.executor.api.CommandLineExecutorService;
039import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
040import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult;
041import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
042import org.nuxeo.runtime.api.Framework;
043
044/**
045 * @since 7.4
046 */
047public class DiffPictures {
048
049    public static final String DEFAULT_COMMAND = "diff-pictures-default";
050
051    public static final String DEFAULT_XPATH = "file:content";
052
053    public static final String DEFAULT_FUZZ = "0";
054
055    public static final String DEFAULT_HIGHLIGHT_COLOR = "Red";
056
057    public static final String DEFAULT_LOWLIGHT_COLOR = "None";
058
059    protected static final String TEMP_DIR_PATH = System.getProperty("java.io.tmpdir");
060
061    // See nuxeo-diff-pictures-template.html
062    protected static final String TMPL_PREFIX = "{{";
063
064    protected static final String TMPL_SUFFIX = "}}";
065
066    protected static final String TMPL_CONTEXT_PATH = buildTemplateKey("CONTEXT_PATH");
067
068    protected static final String TMPL_ACTION = buildTemplateKey("ACTION");
069
070    protected static final String TMPL_LEFT_DOC_ID = buildTemplateKey("LEFT_DOC_ID");
071
072    protected static final String TMPL_LEFT_DOC_LABEL = buildTemplateKey("LEFT_DOC_LABEL");
073
074    protected static final String TMPL_RIGHT_DOC_ID = buildTemplateKey("RIGHT_DOC_ID");
075
076    protected static final String TMPL_RIGHT_DOC_LABEL = buildTemplateKey("RIGHT_DOC_LABEL");
077
078    protected static final String TMPL_XPATH = buildTemplateKey("XPATH");
079
080    protected static final String TMPL_TIME_STAMP = buildTemplateKey("TIME_STAMP");
081
082    Blob b1;
083
084    Blob b2;
085
086    String leftDocId;
087
088    String rightDocId;
089
090    String commandLine;
091
092    Map<String, Serializable> clParameters;
093
094    protected static String buildTemplateKey(String inName) {
095        return TMPL_PREFIX + inName + TMPL_SUFFIX;
096    }
097
098    public DiffPictures(Blob inB1, Blob inB2) {
099
100        this(inB1, inB2, null, null);
101
102    }
103
104    public DiffPictures(DocumentModel inLeft, DocumentModel inRight) {
105
106        this(inLeft, inRight, null);
107    }
108
109    public DiffPictures(DocumentModel inLeft, DocumentModel inRight, String inXPath) {
110
111        Blob leftB, rightB;
112
113        if (StringUtils.isBlank(inXPath) || "null".equals(inXPath)) {
114            leftB = (Blob) inLeft.getPropertyValue(DEFAULT_XPATH);
115            rightB = (Blob) inRight.getPropertyValue(DEFAULT_XPATH);
116        } else {
117            leftB = (Blob) inLeft.getPropertyValue(inXPath);
118            rightB = (Blob) inRight.getPropertyValue(inXPath);
119        }
120
121        init(leftB, rightB, inLeft.getId(), inRight.getId());
122    }
123
124    public DiffPictures(Blob inB1, Blob inB2, String inLeftDocId, String inRightDocId) {
125        init(inB1, inB2, inLeftDocId, inRightDocId);
126
127    }
128
129    private void init(Blob inB1, Blob inB2, String inLeftDocId, String inRightDocId) {
130
131        b1 = inB1;
132        b2 = inB2;
133        leftDocId = inLeftDocId;
134        rightDocId = inRightDocId;
135    }
136
137    public Blob compare(String inCommandLine, Map<String, Serializable> inParams) throws CommandNotAvailable,
138            IOException {
139
140        String finalName;
141
142        commandLine = StringUtils.isBlank(inCommandLine) ? DEFAULT_COMMAND : inCommandLine;
143
144        clParameters = inParams == null ? new HashMap<>() : inParams;
145
146        finalName = (String) clParameters.get("targetFileName");
147        if (StringUtils.isBlank(finalName)) {
148            finalName = "comp-" + b1.getFilename();
149        }
150
151        CloseableFile cf1 = null, cf2 = null;
152        String filePath1 = null, filePath2 = null;
153        CommandLineExecutorService cles = Framework.getService(CommandLineExecutorService.class);
154        CmdParameters params = cles.getDefaultCmdParameters();
155
156        try {
157            cf1 = b1.getCloseableFile();
158            filePath1 = cf1.getFile().getAbsolutePath();
159            params.addNamedParameter("file1", filePath1);
160
161            cf2 = b2.getCloseableFile();
162            filePath2 = cf2.getFile().getAbsolutePath();
163            params.addNamedParameter("file2", filePath2);
164
165            checkDefaultParametersValues();
166            for (Entry<String, Serializable> entry : clParameters.entrySet()) {
167                params.addNamedParameter(entry.getKey(), (String) entry.getValue());
168            }
169
170            String destFilePath;
171            String destFileExtension;
172
173            int dotIndex = finalName.lastIndexOf('.');
174            if (dotIndex < 0) {
175                destFileExtension = ".tmp";
176            } else {
177                destFileExtension = finalName.substring(dotIndex);
178            }
179
180            Blob tempBlob = Blobs.createBlobWithExtension(destFileExtension);
181            destFilePath = tempBlob.getFile().getAbsolutePath();
182
183            params.addNamedParameter("targetFilePath", destFilePath);
184
185            ExecResult execResult = cles.execCommand(commandLine, params);
186
187            // WARNING
188            // ImageMagick can return a non zero code with some of its commands,
189            // while the execution went totally OK, with no error. The problem is
190            // that the CommandLineExecutorService assumes a non-zero return code is
191            // an error => we must handle the thing by ourselves, basically just
192            // checking if we do have a comparison file created by ImageMagick
193            File tempDestFile = tempBlob.getFile();
194            if (!tempDestFile.exists() || tempDestFile.length() < 1) {
195                throw new NuxeoException("Failed to execute the command <" + commandLine + ">. Final command [ "
196                        + execResult.getCommandLine() + " ] returned with error " + execResult.getReturnCode(),
197                        execResult.getError());
198            }
199
200            tempBlob.setFilename(finalName);
201
202            return tempBlob;
203
204        } catch (IOException e) {
205            if (filePath1 == null) {
206                throw new IOException("Could not get a valid File from left blob.", e);
207            }
208            if (filePath2 == null) {
209                throw new IOException("Could not get a valid File from right blob.", e);
210            }
211
212            throw e;
213
214        } finally {
215            if (cf1 != null) {
216                cf1.close();
217            }
218            if (cf2 != null) {
219                cf2.close();
220            }
221        }
222    }
223
224    /*
225     * Adds the default values if a parameter is missing. This applies for all command lines (and some will be unused)
226     */
227    protected void checkDefaultParametersValues() {
228
229        if (isDefaultValue((String) clParameters.get("fuzz"))) {
230            clParameters.put("fuzz", DEFAULT_FUZZ);
231        }
232
233        if (isDefaultValue((String) clParameters.get("highlightColor"))) {
234            clParameters.put("highlightColor", DEFAULT_HIGHLIGHT_COLOR);
235        }
236
237        if (isDefaultValue((String) clParameters.get("lowlightColor"))) {
238            clParameters.put("lowlightColor", DEFAULT_LOWLIGHT_COLOR);
239        }
240
241    }
242
243    protected boolean isDefaultValue(String inValue) {
244        return StringUtils.isBlank(inValue) || inValue.toLowerCase().equals("default");
245    }
246
247    public static String buildDiffHtml(DocumentModel leftDoc, DocumentModel rightDoc, String xpath) throws IOException {
248        String html = "";
249        InputStream in = null;
250        try {
251            in = DiffPictures.class.getResourceAsStream("/files/nuxeo-diff-pictures-template.html");
252            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
253            String line;
254            while ((line = reader.readLine()) != null) {
255                html += line + "\n";
256            }
257
258        } finally {
259            if (in != null) {
260                in.close();
261            }
262        }
263
264        String leftDocId = leftDoc.getId();
265        String rightDocId = rightDoc.getId();
266        String leftLabel = leftDoc.getTitle();
267        String rightLabel = rightDoc.getTitle();
268        if (leftLabel.equals(rightLabel)) {
269            if (leftDoc.isVersion()) {
270                leftLabel = "Version " + leftDoc.getVersionLabel();
271            }
272            if (rightDoc.isVersion()) {
273                rightLabel = "Version " + rightDoc.getVersionLabel();
274            }
275        }
276
277        html = html.replace(TMPL_CONTEXT_PATH, VirtualHostHelper.getContextPathProperty());
278        html = html.replace(TMPL_ACTION, "diff");
279        html = html.replace(TMPL_LEFT_DOC_ID,
280                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(leftDocId)));
281        html = html.replace(TMPL_LEFT_DOC_LABEL,
282                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(leftLabel)));
283        html = html.replace(TMPL_RIGHT_DOC_ID,
284                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(rightDocId)));
285        html = html.replace(TMPL_RIGHT_DOC_LABEL,
286                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(rightLabel)));
287        if (StringUtils.isBlank(xpath) || xpath.toLowerCase().equals("default")) {
288            xpath = DEFAULT_XPATH;
289        }
290        html = html.replace(TMPL_XPATH, StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(xpath)));
291        // dc:modified can be null... When running the Unit Tests for example
292        String lastModification;
293        Calendar cal = (Calendar) rightDoc.getPropertyValue("dc:modified");
294        if (cal == null) {
295            lastModification = "" + Calendar.getInstance().getTimeInMillis();
296        } else {
297            lastModification = "" + cal.getTimeInMillis();
298        }
299        html = html.replace(TMPL_TIME_STAMP, lastModification);
300
301        return html;
302    }
303
304}