Commits

P_W999 committed 3c50e33

- added TimeShift functionality
- update copyright message on some files

Comments (0)

Files changed (21)

src/be/pw/jexif/DemoApp.java

 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import be.pw.jexif.enums.DateTag;
 import be.pw.jexif.enums.tag.ExifIFD;
 import be.pw.jexif.exception.ExifError;
 import be.pw.jexif.exception.JExifException;
 import be.pw.jexif.internal.constants.ExecutionConstant;
+import be.pw.jexif.internal.util.TimeShiftGenerator;
 
 import com.google.common.base.Stopwatch;
 import com.google.common.io.Files;
 		JExifInfo info4 = tool.getInfo(new File("test-resources/read04.JPG"));
 		File tmp = File.createTempFile("jexiftool", ".jpg");
 		File gps = File.createTempFile("gpstest", ".jpg");
+		File shift = File.createTempFile("gpstest", ".jpg");
 		tmp.deleteOnExit();
 		// gps.deleteOnExit();
 		Files.copy(new File("test-resources/read04.JPG"), tmp);
 		Files.copy(new File("test-resources/read04.JPG"), gps);
+		Files.copy(new File("test-resources/read04.JPG"), shift);
 
 		// tool.stop();
 		JExifInfo write1 = tool.getInfo(tmp);
 		/*
 		 * System.out.println(Cal10nUtil.get(Errors.VALIDATION_RAT_FOR_INT)); Cal10nUtil.changeLocale(Locale.ENGLISH); System.out.println(Cal10nUtil.get(Errors.VALIDATION_RAT_FOR_INT)); Cal10nUtil.changeLocale(new Locale("nl")); System.out.println(Cal10nUtil.get(Errors.VALIDATION_RAT_FOR_INT));
 		 */
-
+		JExifInfo shiftWrite = tool.getInfo(shift);
+		LOG.info("Original timestamp: " + shiftWrite.getTagValue(ExifIFD.CREATEDATE));
+		LOG.info("Shifted timestamp: " + shiftWrite.timeShift(DateTag.CREATEDATE, TimeShiftGenerator.generateTimeShift(true, 1, 0, 0)));
+		
 		LOG.info("Executing took {} ms", watch.elapsedTime(TimeUnit.MILLISECONDS));
 		LOG.info("Finished main method");
 	}

src/be/pw/jexif/JExifInfo.java

 import java.util.HashMap;
 import java.util.Map;
 
+import be.pw.jexif.enums.DateTag;
 import be.pw.jexif.enums.tag.ExifGPS;
 import be.pw.jexif.enums.tag.Tag;
 import be.pw.jexif.exception.ExifError;
 	public Map<Tag, String> getAllSupportedExactTagsValues() throws ExifError, JExifException {
 		return exifTool.getAllSupportedTagInfo(file, true);
 	}
+	
+	@Beta
+	public String timeShift(final DateTag dateTag, final String shiftPattern) throws JExifException, ExifError {
+	    exifTool.timeShift(file, shiftPattern, dateTag);
+	    return getTagValue(dateTag.getTag());
+	}
 
 }

src/be/pw/jexif/JExifTool.java

 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import be.pw.jexif.enums.DateTag;
 import be.pw.jexif.enums.Errors;
 import be.pw.jexif.enums.tag.ExifGPS;
 import be.pw.jexif.enums.tag.ExifIFD;
  * In order to use J-ExifTool you must download ExifTool from <a href="http://www.sno.phy.queensu.ca/~phil/exiftool/">http://www.sno.phy.queensu.ca/~phil/exiftool/</a> <br />
  * The System Property {@link ExecutionConstant#EXIFTOOLPATH} should point to the executable.<br />
  * <br />
- * If you want to read or write Exif tags, you first need to make an instance of this class and the use the {@link #getInfo(File)) method to create a {@link JExifInfo} object. <br />
+ * If you want to read or write Exif tags, you first need to make an instance of this class and then use the {@link #getInfo(File)) method to create a {@link JExifInfo} object. <br />
  * <b>JExifTool is not thread-safe</b>
+ * <p>
+ * The following System Properties are used by J-ExifTool and may or must be set for proper operation:
+ * <ul>
+ *  <li>{@link ExecutionConstant#EXIFTOOLPATH}: the path to the ExifTool executable</li>
+ *  <li>{@link ExecutionConstant#EXIFTOOLDEADLOCK}: timeout</li>
+ *  <li>{@link ExecutionConstant#EXIFTOOLBYPASSVALIDATION}: whether to bypass validations when writing tags (set to true to bypass, use at own risk)</li>
+ * </ul>
  * @author phillip
  */
 @Beta
 	Map<Tag, String> getAllSupportedTagInfo(final File file, final boolean exact) throws ExifError, JExifException {
 		return (Map<Tag, String>) getAllTagInfo(file, exact, true);
 	}
+	
+	void timeShift(final File file, final String shift, final DateTag tag) throws JExifException, ExifError {
+	    startIfNecessary();
+        LOG.trace("Starting timeShift");
+        int i = 0;
+        IAction action;
+        EventHandler handler;
+        try {
+            action = ActionFactory.createDateTimeShiftAction(file, shift, tag);
+            handler = new EventHandler();
+            bus.register(handler);
+
+            String[] arguments = action.buildArguments();
+            for (String argument : arguments) {
+                argsWriter.append(argument).append("\r\n");
+            }
+            argsWriter.flush();
+            while (!handler.isFinished() && i <= deadLock) {
+                Thread.sleep(50);
+                i += 50;
+            }
+            if (!handler.isFinished()) {
+                LOG.error(Cal10nUtil.get(Errors.DEADLOCK, i));
+                throw new JExifException(Cal10nUtil.get(Errors.DEADLOCK, i));
+            }
+            bus.unregister(handler);
+        } catch (IOException e) {
+            throw new JExifException(Cal10nUtil.get(Errors.IO_ARGSFILE), e);
+        } catch (InterruptedException e) {
+            throw new JExifException(Cal10nUtil.get(Errors.INTERRUPTED_SLEEP), e);
+        }
+        List<String> results = handler.getResultList();
+        List<String> errors = handler.getErrorList();
+        ResultHandler.run(action, results, errors);
+	}
 
 	private Map<? extends Object, String> getAllTagInfo(final File file, final boolean exact, final boolean onlySupported) throws ExifError, JExifException {
 		startIfNecessary();

src/be/pw/jexif/enums/DateTag.java

+/*******************************************************************************
+ * Copyright 2013 P_W999
+ * 
+ * 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 be.pw.jexif.enums;
+
+import be.pw.jexif.JExifInfo;
+import be.pw.jexif.enums.tag.ExifIFD;
+import be.pw.jexif.enums.tag.IFD0;
+import be.pw.jexif.enums.tag.Tag;
+
+/**
+ * This enumeration contains a list of Tags which can be timeshifted.
+ * <p>
+ * The ALLDATES tag is an ExifTool shortcut for:
+ * <ul>
+ *  <li>DateTimeOriginal
+ *  <li>CreateDate
+ *  <li>ModifyDate
+ * </ul>
+ * See also {@link JExifInfo#timeShift(DateTag, String)}
+ * @author phillip
+ * @since v0.0.6
+ */
+public enum DateTag {
+
+    DATETIMEORIGINAL(ExifIFD.DATETIMEORIGINAL), CREATEDATE(ExifIFD.CREATEDATE), MODIFYDATE(IFD0.MODIFYDATE), ALLDATES(null);
+    
+    private Tag tag;
+    
+    private DateTag(Tag tag) {
+        this.tag = tag;
+    }
+    
+    public Tag getTag() {
+        return this.tag;
+    }
+}

src/be/pw/jexif/enums/Errors.java

 @BaseName("errorMessages")
 @LocaleData({ @Locale("en") })
 public enum Errors {
-	EXIFTOOL_INVALID_PATH, GENERAL, EXIFTHREAD, IO_ARGSFILE, IO_CLOSING, INTERRUPTED_SLEEP, DEADLOCK, VALIDATION_TAGLIST, IO_BUILD_ARGUMENTS, VALIDATION_VALUESLIST, EXIFTOOL_EXECUTION_ERROR, EXIFTOOL_EXECUTION_WARNING, RETREIVE_PARSER_ERROR, PARSER_UNKNOWNTYPE, PARSER_MISMATCH, VALIDATION_WRITE_PROTECTED_TAG, VALIDATION_WRITE_AVOIDED_TAG, VALIDATION_WRITE_UNSAFE_TAG, UNKNOWN_DATATYPE, VALIDATION_NEGATIVE_UNSIGNED, VALIDATION_EXCEEDS_LIMITS, VALIDATION_RAT_FOR_INT, VALIDATION_INVALID_TYPE, IO_FILE_NOT_VALID, GPS_MISSING_FIELD
+	EXIFTOOL_INVALID_PATH, GENERAL, EXIFTHREAD, IO_ARGSFILE, IO_CLOSING, INTERRUPTED_SLEEP, DEADLOCK, VALIDATION_TAGLIST, IO_BUILD_ARGUMENTS, VALIDATION_VALUESLIST, EXIFTOOL_EXECUTION_ERROR, EXIFTOOL_EXECUTION_WARNING, RETREIVE_PARSER_ERROR, PARSER_UNKNOWNTYPE, PARSER_MISMATCH, VALIDATION_WRITE_PROTECTED_TAG, VALIDATION_WRITE_AVOIDED_TAG, VALIDATION_WRITE_UNSAFE_TAG, UNKNOWN_DATATYPE, VALIDATION_NEGATIVE_UNSIGNED, VALIDATION_EXCEEDS_LIMITS, VALIDATION_RAT_FOR_INT, VALIDATION_INVALID_TYPE, IO_FILE_NOT_VALID, GPS_MISSING_FIELD, VALIDATION_TIMESHIFT
 }

src/be/pw/jexif/internal/action/IDateTimeShiftAction.java

+/**
+ * 
+ */
+package be.pw.jexif.internal.action;
+
+/**
+ * Interface for timeshift functionality.
+ * @author phillip
+ * @since v0.0.6
+ *
+ */
+public interface IDateTimeShiftAction extends IAction {
+
+}

src/be/pw/jexif/internal/action/impl/ActionFactory.java

 import java.io.IOException;
 import java.util.Map;
 
+import be.pw.jexif.enums.DateTag;
 import be.pw.jexif.enums.tag.Tag;
 import be.pw.jexif.exception.JExifValidationException;
+import be.pw.jexif.internal.action.IDateTimeShiftAction;
 import be.pw.jexif.internal.action.ITagReadAction;
 import be.pw.jexif.internal.action.ITagReadAllAction;
 import be.pw.jexif.internal.action.ITagReadExactAllAction;
 import be.pw.jexif.internal.action.ITagWriteAction;
 
+import com.google.common.collect.Sets;
+
 /**
  * Factory for the different action.
  * @see be.pw.jexif.internal.action.IAction
 		readExactAll.setParams(file);
 		return readExactAll;
 	}
+	
+	public static IDateTimeShiftAction createDateTimeShiftAction(final File file, String shift, DateTag ... tags) throws IOException {
+	    DateTimeShiftAction dateTimeShift = new DateTimeShiftAction();
+	    dateTimeShift.setParams(file, shift, Sets.newHashSet(tags));
+	    return dateTimeShift;
+	}
 }

src/be/pw/jexif/internal/action/impl/DateTimeShiftAction.java

+/*******************************************************************************
+ * Copyright 2013 P_W999
+ * 
+ * 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 be.pw.jexif.internal.action.impl;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collection;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import be.pw.jexif.enums.DateTag;
+import be.pw.jexif.enums.Errors;
+import be.pw.jexif.exception.JExifException;
+import be.pw.jexif.internal.action.AbstractAction;
+import be.pw.jexif.internal.action.IDateTimeShiftAction;
+import be.pw.jexif.internal.constants.ExecutionConstant;
+import be.pw.jexif.internal.util.ArrayUtil;
+import be.pw.jexif.internal.util.Cal10nUtil;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+/**
+ * Action to perform date/time-shifts.
+ * <p>
+ * Usefull documentation can be found at {@link http://www.sno.phy.queensu.ca/~phil/exiftool/Shift.html}.
+ * @author phillip
+ * @since v0.0.6
+ *
+ */
+class DateTimeShiftAction extends AbstractAction implements IDateTimeShiftAction {
+
+    /**
+     * Hidden constructor. All initializations should occur from the {@link ActionFactory}.
+     */
+    DateTimeShiftAction() { 
+        super();
+    }
+    
+    /**
+     * The logger for this class.
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(DateTimeShiftAction.class);
+    
+    /**
+     * The shift to perform without the shift direction
+     */
+    private String shift;
+    
+    /**
+     * The shift direction (either + or -)
+     */
+    private String shiftDirection;
+    
+    /**
+     * The list of date tags which must be modified
+     */
+    private Collection<DateTag> datesToModify;
+    
+    /**
+     * Sets the params.
+     * @param file the file from which you would like to change the Exif tag values.
+     * @param shift a string containing the amount to be shifted. The format should be accepted by ExifTool {@link http://www.sno.phy.queensu.ca/~phil/exiftool/Shift.html}
+     * @param datesToModify a list with DateTags which should be time-shifted
+     * @throws JExifException if the give value is not of a valid type.
+     * @throws IOException if the given file does not exist.
+     */
+    public void setParams(final File file, final String shift, final Collection<DateTag> datesToModify) throws IOException {
+        Preconditions.checkNotNull(file);
+        if (!file.exists()) {
+            throw new FileNotFoundException(Cal10nUtil.get(Errors.IO_FILE_NOT_VALID, file.getAbsolutePath()));
+        }
+        Preconditions.checkNotNull(shift);
+        Preconditions.checkArgument(shift.matches("[\\d:\\+\\- ]*"), Cal10nUtil.get(Errors.VALIDATION_TIMESHIFT, shift));
+        Preconditions.checkNotNull(datesToModify);
+        Preconditions.checkArgument(datesToModify.size() != 0, Cal10nUtil.get(Errors.VALIDATION_TAGLIST));
+        this.file = file;
+        if (shift.startsWith("+")) {
+            shiftDirection = "+";
+            this.shift = shift.substring(1);  
+        } else if (shift.startsWith("-")) {
+            shiftDirection = "-";
+            this.shift = shift.substring(1);
+        } else {
+            shiftDirection = "+";
+            this.shift = shift;
+        }
+        this.datesToModify = datesToModify;
+        if (datesToModify.contains(DateTag.ALLDATES)) {
+            datesToModify.removeAll(Lists.newArrayList(DateTag.CREATEDATE, DateTag.DATETIMEORIGINAL, DateTag.MODIFYDATE));  //ensure no doubles
+        }
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String[] buildArguments() throws JExifException {
+        try {
+            int size = datesToModify.size() + 7;
+            String[] arguments = new String[size];
+            int i = 0;
+            arguments[i++] = ExecutionConstant.ECHO;
+            arguments[i++] = ExecutionConstant.START + " " + getId();
+            for (DateTag tag : datesToModify) {
+                arguments[i++] = "-" + tag.getTag().getName() + shiftDirection+ "=" + shift;
+            }
+            arguments[i++] = ExecutionConstant.EXACT_VALUE;
+            arguments[i++] = file.getCanonicalPath();
+            arguments[i++] = ExecutionConstant.ECHO;
+            arguments[i++] = ExecutionConstant.STOP + " " + getId();
+            arguments[i] = ExecutionConstant.EXECUTE;
+            LOG.trace("Arguments are : " + ArrayUtil.toString(arguments));
+            return arguments;
+        } catch (IOException e) {
+            throw new JExifException(Cal10nUtil.get(Errors.IO_BUILD_ARGUMENTS), e);
+        }
+    }
+
+}

src/be/pw/jexif/internal/result/ResultHandler.java

 				if (!s.toLowerCase().contains("warning")) {
 					onlyWarnings = false;
 					b.append(" > ").append(s);
+				} else {
+				    LOG.warn(s);
 				}
 			}
 			if (!onlyWarnings) {

src/be/pw/jexif/internal/result/impl/DateTimeShiftParser.java

+/**
+ * 
+ */
+package be.pw.jexif.internal.result.impl;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+
+import be.pw.jexif.enums.Errors;
+import be.pw.jexif.internal.action.IAction;
+import be.pw.jexif.internal.action.IDateTimeShiftAction;
+import be.pw.jexif.internal.constants.ExecutionConstant;
+import be.pw.jexif.internal.result.IResultParser;
+import be.pw.jexif.internal.util.Cal10nUtil;
+
+/**
+ * Parses the result for the {@link IDateTimeShiftAction}.
+ * @author phillip
+ * @since v0.0.6
+ */
+class DateTimeShiftParser implements IResultParser {
+    
+    /**
+     * Logger for this class.
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(DateTimeShiftParser.class);
+    
+    @Override
+    public void parse(IAction action, String actionUID, List<String> result) {
+        Preconditions.checkArgument(validFor().isAssignableFrom(action.getClass()), Cal10nUtil.get(Errors.PARSER_MISMATCH, getClass().getSimpleName(), action.getClass().getSimpleName()));
+        Preconditions.checkArgument(action.getId().equals(actionUID), "Invalid action UID");
+
+        for (String line : result) {
+            if (!line.contains(ExecutionConstant.EXIFTOOLREADY) && !line.isEmpty()) {
+                LOG.trace(line);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Class<? extends IAction> validFor() {
+        return IDateTimeShiftAction.class;
+    }
+
+}

src/be/pw/jexif/internal/result/impl/ParserFactory.java

 import be.pw.jexif.enums.Errors;
 import be.pw.jexif.exception.JExifException;
 import be.pw.jexif.internal.action.IAction;
+import be.pw.jexif.internal.action.IDateTimeShiftAction;
 import be.pw.jexif.internal.action.ITagReadAction;
 import be.pw.jexif.internal.action.ITagReadAllAction;
 import be.pw.jexif.internal.action.ITagReadExactAction;
 		if (action instanceof ITagReadExactAllAction) {
 			return new TagReadExactAllParser();
 		}
+		if (action instanceof IDateTimeShiftAction) {
+		    return new DateTimeShiftParser();
+		}
 		throw new JExifException(Cal10nUtil.get(Errors.PARSER_UNKNOWNTYPE, action.getClass().getName()));
 	}
 }

src/be/pw/jexif/internal/thread/JExifOutputStream.java

-package be.pw.jexif.internal.thread;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-import be.pw.jexif.internal.thread.event.ErrorEvent;
-import be.pw.jexif.internal.thread.event.InputEvent;
-
-import com.google.common.eventbus.EventBus;
-
-public class JExifOutputStream extends OutputStream {
-
-	private EventBus bus;
-	private boolean error;
-	private String buff = null;
-
-	public JExifOutputStream(final EventBus bus, final boolean error) throws IOException {
-		this.bus = bus;
-		this.error = error;
-	}
-
-	@Override
-	public void write(final int b) throws IOException {
-
-	}
-
-	@Override
-	public void write(final byte[] b, final int off, final int len) throws IOException {
-		StringBuilder build = new StringBuilder();
-		if (buff != null) {
-			build.append(buff);
-		}
-		for (int i = 0; i < len; ++i) { // not newline character
-			if (b[i] != '\r' && b[i] != '\n') {
-				build.append((char) b[i]);
-			} else if ((i + 1) == len || (b[i + 1] != '\r' && b[i + 1] != '\n')) { // current = newline, next isn't newline OR at the end of buffer
-				if (error) {
-					bus.post(new ErrorEvent(build.toString()));
-				} else {
-					bus.post(new InputEvent(build.toString()));
-				}
-				build = new StringBuilder();
-			}
-		}
-		if (!build.toString().isEmpty()) {
-			buff = build.toString();
-		} else {
-			buff = null;
-		}
-	}
-
-}
+/*******************************************************************************
+ * Copyright 2013 P_W999
+ * 
+ * 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 be.pw.jexif.internal.thread;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import be.pw.jexif.internal.thread.event.ErrorEvent;
+import be.pw.jexif.internal.thread.event.InputEvent;
+
+import com.google.common.eventbus.EventBus;
+
+public class JExifOutputStream extends OutputStream {
+
+	private EventBus bus;
+	private boolean error;
+	private String buff = null;
+
+	public JExifOutputStream(final EventBus bus, final boolean error) throws IOException {
+		this.bus = bus;
+		this.error = error;
+	}
+
+	@Override
+	public void write(final int b) throws IOException {
+
+	}
+
+	@Override
+	public void write(final byte[] b, final int off, final int len) throws IOException {
+		StringBuilder build = new StringBuilder();
+		if (buff != null) {
+			build.append(buff);
+		}
+		for (int i = 0; i < len; ++i) { // not newline character
+			if (b[i] != '\r' && b[i] != '\n') {
+				build.append((char) b[i]);
+			} else if ((i + 1) == len || (b[i + 1] != '\r' && b[i + 1] != '\n')) { // current = newline, next isn't newline OR at the end of buffer
+				if (error) {
+					bus.post(new ErrorEvent(build.toString()));
+				} else {
+					bus.post(new InputEvent(build.toString()));
+				}
+				build = new StringBuilder();
+			}
+		}
+		if (!build.toString().isEmpty()) {
+			buff = build.toString();
+		} else {
+			buff = null;
+		}
+	}
+
+}

src/be/pw/jexif/internal/util/TimeShiftGenerator.java

+/*******************************************************************************
+ * Copyright 2013 P_W999
+ * 
+ * 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 be.pw.jexif.internal.util;
+
+/**
+ * A utility class to create a timeshift string.
+ * <p>
+ * A timeshift can have many format as described here: {@link http://www.sno.phy.queensu.ca/~phil/exiftool/Shift.html}
+ * <p>
+ * The TimeShiftGenerator will only generated two simple and straightforward shifts:
+ * <ul>
+ *  <li>[+/-]h:m:s or [+/-]Y:M:D
+ *  <li>Y:M:D h:m:s:[+/-]h
+ * </ul>
+ * @author phillip
+ * @since v0.0.6
+ *
+ */
+public final class TimeShiftGenerator {
+    /**
+     * Separator between hour, min, sec or year, month, day
+     */
+    private static final String SEPARATOR = ":";
+    
+    /**
+     * Generates a timeshift String in the format Y:M:D or h:m:s
+     * <p>
+     * The positive parameter will defined whether it's a forward or backward shift. 
+     * All passed arguments will be interpreted as positive numbers.
+     * @param positive whether the timeshift is towards the future (true) or to the past (false)
+     * @param largest either hour or year
+     * @param inbetween either minute or month
+     * @param smallest either second or day
+     * @return the formatted string
+     */
+    public static final String generateTimeShift(boolean positive, int largest, int inbetween, int smallest) {
+        StringBuilder b = new StringBuilder();
+        if (positive) {
+            b.append("+");
+        } else {
+            b.append("-");
+        }
+        b.append(base(largest)).append(SEPARATOR);
+        b.append(base(inbetween)).append(SEPARATOR);
+        b.append(base(smallest));
+        return b.toString();
+    }
+    
+    /**
+     * Generates a timeshift String in the format Y:M:D h:m:s:[+/-]h
+     * All passed arguments will be interpreted as positive numbers, except for the timezone.
+     * @param positive whether the timeshift is towards the future (true) or to the past (false)
+     * @param year year
+     * @param month month
+     * @param day day
+     * @param hour hour 
+     * @param minute minute
+     * @param second second
+     * @param timezone timezone hour shift (can be positive or negative)
+     * @return the formatted string
+     */
+    public static final String generateTimeShift(boolean positive, int year, int month, int day, int hour, int minute, int second, int timezone) {
+        StringBuilder b = new StringBuilder();
+        b.append(generateTimeShift(positive, year, month, day));
+        b.append(" ");
+        b.append(generateTimeShift(positive, hour, minute, second).substring(1));
+        b.append(SEPARATOR);
+        if (timezone != 0) {
+            if (timezone > 0) {
+                b.append("+").append(timezone);
+            } else {
+                b.append(timezone);
+            }
+        }
+        return b.toString();
+    }
+    
+    
+    static final int base(int i) {
+        if (i < 0) {
+            return -1 * i;
+        } else {
+            return i;
+        }
+    }
+}

src/errorMessages.properties

 VALIDATION_RAT_FOR_INT=Tag {0} has datatype {1}. The value {2} however seems to be a rational.
 VALIDATION_INVALID_TYPE=Tag {0} has datatype {1}. The value {2} could not be parsed as being such.
 IO_FILE_NOT_VALID=There was a problem reading the file {0}
-GPS_MISSING_FIELD=Not all mandatory GPS field were provided (needs: GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef, GPSAltitude and GPSAltitudeRef)
+GPS_MISSING_FIELD=Not all mandatory GPS field were provided (needs: GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef, GPSAltitude and GPSAltitudeRef)
+VALIDATION_TIMESHIFT=The provided time shift "{0}" is not valid. The shift may only contain numbers, ' ', '+', '-' and ':' .

src/errorMessages_en.properties

 VALIDATION_RAT_FOR_INT=Tag {0} has datatype {1}. The value {2} however seems to be a rational.
 VALIDATION_INVALID_TYPE=Tag {0} has datatype {1}. The value {2} could not be parsed as being such.
 IO_FILE_NOT_VALID=There was a problem reading the file {0}
-GPS_MISSING_FIELD=Not all mandatory GPS field were provided (needs: GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef, GPSAltitude and GPSAltitudeRef)
+GPS_MISSING_FIELD=Not all mandatory GPS field were provided (needs: GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef, GPSAltitude and GPSAltitudeRef)
+VALIDATION_TIMESHIFT=The provided time shift "{0}" is not valid. The shift may only contain numbers, ' ', '+', '-' and ':' .

src/errorMessages_nl.properties

 VALIDATION_RAT_FOR_INT=Tag {0} heeft datatype {1}. De waarde {2} is echter rationieel.
 VALIDATION_INVALID_TYPE=Tag {0} heeft datatype {1}. De waarde {2} kon niet als zo zijnde geparsed worden.
 IO_FILE_NOT_VALID=Er was een probleem met het uitlezen van het bestand {0}
-GPS_MISSING_FIELD=Niet alle verplichte GPS velden zijn gegeven (needs: GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef, GPSAltitude and GPSAltitudeRef)
+GPS_MISSING_FIELD=Niet alle verplichte GPS velden zijn gegeven (needs: GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef, GPSAltitude and GPSAltitudeRef)
+VALIDATION_TIMESHIFT=De opgegeven time shift "{0}" is niet geldig. De shift mag enkel cijfers, ' ', '+', '-' and ':' bevatten.

src/jexiftool.properties

 jexiftool.version=0.0.3
-exiftool.path=lib\\exiftool.exe
+exiftool.path=/home/phillip/bin/exiftool/exiftool
 exiftool.deadlock=4000

src/log4j.properties

 
 #log4j.logger.be.pw=DEBUG
 log4j.logger.be.pw.jexif.internal.thread.event.DebugHandler=FATAL
-log4j.logger.be.pw.jexif.DemoApp=ERROR
+log4j.logger.be.pw.jexif.DemoApp=INFO
 log4j.logger.be.pw.jexif.JExifTool=INFO
 
 

test/be/pw/jexif/internal/util/GPSUtilTest.java

+/*******************************************************************************
+ * Copyright 2013 P_W999
+ * 
+ * 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 be.pw.jexif.internal.util;
 
 import static org.fest.assertions.Assertions.assertThat;

test/be/pw/jexif/internal/util/TagUtilTest.java

+/*******************************************************************************
+ * Copyright 2013 P_W999
+ * 
+ * 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 be.pw.jexif.internal.util;
 
 import org.testng.annotations.Test;

test/be/pw/jexif/internal/util/TimeShiftGeneratorTest.java

+/*******************************************************************************
+ * Copyright 2013 P_W999
+ * 
+ * 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 be.pw.jexif.internal.util;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.testng.annotations.Test;
+
+public class TimeShiftGeneratorTest {
+
+    @Test
+    public void testBase() {
+        assertThat(TimeShiftGenerator.base(1)).as("Positive value should remain positive").isEqualTo(1);
+        assertThat(TimeShiftGenerator.base(-1)).as("Negative value should become positive").isEqualTo(1);
+        assertThat(TimeShiftGenerator.base(0)).as("0 should remain untouched").isEqualTo(0);
+    }
+
+
+    @Test
+    public void generateTimeShiftbooleanintintint() {
+        assertThat(TimeShiftGenerator.generateTimeShift(true, 2, 3, 4)).as("Generated dateshift value should match").isEqualTo("+2:3:4");
+        assertThat(TimeShiftGenerator.generateTimeShift(false, 2, 3, 4)).as("Generated dateshift value should match").isEqualTo("-2:3:4");
+    }
+
+    @Test
+    public void generateTimeShiftbooleanintintintintintintint() {
+       assertThat(TimeShiftGenerator.generateTimeShift(true, 1, 2, 3, 4, 5, 6, 7)).as("Generate date/time shift value should match").isEqualTo("+1:2:3 4:5:6:+7");
+       assertThat(TimeShiftGenerator.generateTimeShift(false, 1, 2, 3, 4, 5, 6, 7)).as("Generate date/time shift value should match").isEqualTo("-1:2:3 4:5:6:+7");
+       assertThat(TimeShiftGenerator.generateTimeShift(false, 1, 2, 3, 4, 5, 6, -7)).as("Generate date/time shift value should match").isEqualTo("-1:2:3 4:5:6:-7");
+    }
+}