Zum Inhalt

Kategorie: Java

SQLite und Java auf dem Raspberry Pi 3

Es ist nicht ganz so einfach, auf dem Raspberry Pi 3 unter Linux mit Java auf eine SQLite Datenbank zuzugreifen, musste ich feststellen. Scheinbar muss eine passende Version von libsqlitejdbc.so für ARM vorhanden sein, was nicht immer der Fall ist. Ansonsten fliegen z.B. Exceptions à la

Error loading native library: /org/sqlite/native/Linux/arm/libsqlitejdbc.so

oder

java.lang.UnsatisfiedLinkError: org.sqlite.core.NativeDB._open(Ljava/lang/String;I)V

Funktioniert hat bei mir letztendlich die Version 3.8.10.2, die es hier zum Download gibt, oder so via Dependency in der pom.xml eingebunden werden kann:

<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.8.10.2</version>
</dependency>

Hot Code Replacement mit Spring Boot

Eine einfache Webapplikation mit Spring Boot ist mit eclipse innerhalb weniger Minuten aufgesetzt und lauffähig. Leider funktioniert bei mir dann kein Hot Code Replacement in eclipse, d.h. Änderungen die ich während der Laufzeit der Applikation mache, machen sich nicht bemerkbar. Aber jedes mal stoppen und starten ist auch keine Lösung. Aber dafür gibt es springloaded bei GitHub.

Dieses Plugin kann z.B. als VM Argument beim Start aus eclipse mit angegeben werden und dann werden Änderungen am Code direkt sichtbar:

-javaagent:D:\Work\Java\maven\repo\org\springframework\springloaded\1.2.3.RELEASE\springloaded-1.2.3.RELEASE.jar -noverify

Und auf der Konsole machen sich Änderungen dann u.a. so bemerkbar:

2015-06-06 12:35:05.127 INFO 10880 --- [Loader@58644d46] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String de.denniswilmsmann.docker.helloworld.HelloWorldController.index()
2015-06-06 12:35:05.127 INFO 10880 --- [Loader@58644d46] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity&lt;java.util.Map&lt;java.lang.String, java.lang.Object&gt;&gt; org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-06-06 12:35:05.127 INFO 10880 --- [Loader@58644d46] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=,custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)

Apache CXF Client mit Basic Authentication

Ein einfacher Apache CXF Client mit Basic Authentication lässt sich mit ein paar Zeilen Java Code umsetzen.

Via maven lassen wir uns aus einer WSDL Datei einen Client generieren:

<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-codegen-plugin</artifactId>
    <version>2.7.4</version>
    <executions>
        <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
                <sourceRoot>${project.build.directory}/generated/cxf</sourceRoot>
                <wsdlOptions>
                    <wsdlOption>
                        <!-- hier eine valide WSDL Url angeben -->
                        <wsdl>https://www.denniswilmsmann.de/ws/soap/dummyservice?wsdl</wsdl>
                    </wsdlOption>
                </wsdlOptions>
            </configuration>
            <goals>
                <goal>wsdl2java</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Und die dazugehörige Implementierung mit Basic Authentication (leicht gekürzt):

import javax.xml.ws.BindingProvider;

public class Client {

  public static void main(String[] args) {
    MyWebServiceImpl ws = new MyWebServiceImpl();
    MyWebServiceService client = ws.getMyWebServiceImplPort();

    BindingProvider portBP = (BindingProvider) client;
    portBP.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "username");
    portBP.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, "password");

    List<Result> results = client.getResult(true, 5.0, 1);

    System.out.println(results.size());
  }

}

Random File Picker

Mit diesem paar Zeilen Code ist es mit Java 7 möglich, zufällig Dateien aus einer Ordnerstruktur zu kopieren, z.B. um schnell mal 100 mp3 Dateien aus der Sammlung auf eine CD oder einen USB Stick zu schieben, ohne selbst bei der Auswahl Hand kreativ werden zu müssen.

Hier gibts den Sourcecode und ein fertiges JAR File zum Download: RandomFilePicker

/*
* Created on 30.08.2014
*/
package de.denniswilmsmann.rfp;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

/*
* $Log: $
*/

public class RandomFilePicker {

	private final static List<File> files = new ArrayList<File>();

	public static void main(String[] args) {

		if (args == null || args.length < 4) {
			System.out
					.println("run with java -jar RandomFilePicker.jar <Source Folder> <Destination Folder> <Recursive y/n> <Number of Files>");
			System.out
					.println("run with java -jar RandomFilePicker.jar D:\\MyMusicArchive D:\\RandomMusic y 100");
			return;
		}

		File sourceFolder = new File(args[0]);

		if (!sourceFolder.isDirectory()) {
			System.out.println(args[0] + " is not a folder");
			return;
		}

		File destFolder = new File(args[1]);

		if (!sourceFolder.isDirectory()) {
			System.out.println(args[1] + " is not a folder");
			return;
		}

		boolean recursive = false;
		if (args[2].equalsIgnoreCase("y")) {
			recursive = true;
		}

		int numberOfFiles = Integer.parseInt(args[3]);

		parseFolder(sourceFolder, recursive);

		if (files.size() < numberOfFiles) {
			System.out
					.println("not enough files found, please reduce <Number of Files>");
		} else {
			Set<File> selectedFiles = new HashSet<File>();
			Random generator = new Random();
			while (selectedFiles.size() < numberOfFiles) {
				int i = generator.nextInt(files.size());
				selectedFiles.add(files.get(i));
			}
			for (File f : selectedFiles) {
				File out = new File(destFolder.getAbsolutePath()
						+ File.separator + f.getName());
				try {
					Files.copy(f.toPath(), out.toPath());
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

	}

	private static void parseFolder(File folder, boolean recursive) {
		for (File f : folder.listFiles()) {
			if (f.isFile()) {
				addFile(f);
			} else if (f.isDirectory() && recursive) {
				parseFolder(f, recursive);
			}
		}
	}

	private static void addFile(File f) {
		// System.out.println(files.size() + 1 + ": " + f.getAbsolutePath());
		files.add(f);
	}
}

Fotos mit Zeitstempel umbenennen

Mit den folgenden paar Zeilen Java Code lassen sich Fotos bequemen umbenennen, d.h. der Dateiname wird noch mit dem Zeitstempel aus den Exif Daten ergänzt. Möglich macht das das Framework metadata-extractor.

/*
 * Created on 28.06.2014
 */
package de.denniswilmsmann.renamephotos;

import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;

/*
 * $Log: $
 */
public class RenamePhotos {

  private final DateFormat df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG);
  private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss");

  public static void main(String[] args) {
    File folder = new File("D:\\PhotosToRename\\");

    RenamePhotos ex = new RenamePhotos();
    ex.parse(folder);
  }

  private void parse(File f) {
    if (f.isDirectory()) {
      parseDirectory(f);
    } else {
      parseFile(f);
    }
  }

  private void parseDirectory(File f) {
    for (File file : f.listFiles()) {
      if (file.isDirectory()) {
        parseDirectory(file);
      } else {
        parseFile(file);
      }
    }
  }

  private void parseFile(File f) {
    try {
      if (f.isFile() && isRenameRequired(f)) {
        Metadata metadata = ImageMetadataReader.readMetadata(f);
        ExifSubIFDDirectory directory = metadata.getDirectory(ExifSubIFDDirectory.class);
        Date date = directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);

        String newName = sdf.format(date) + "_" + f.getName();
        System.out.println(f.getName() + " -> " + newName);

        f.renameTo(new File(f.getParent() + "\\" + newName));
      } else {
        System.out.println("skip " + f.getAbsolutePath());
      }
    } catch (ImageProcessingException e) {
      System.out.println("error parsing " + f.getAbsolutePath());
    } catch (IOException e) {
      System.out.println("error parsing " + f.getAbsolutePath());
    } catch (NullPointerException e) {
      System.out.println("error parsing " + f.getAbsolutePath());
    }
  }

  private boolean isRenameRequired(File f) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < f.getName().length(); i++) {
      sb.append(f.getName().charAt(i));
      try {
        sdf.parse(sb.toString());
        return false;
      } catch (ParseException e) {
        // ignore
      }

    }
    return true;
  }
}

Und dazu noch die passsende pom.xml, damit die Dependencies einfach via maven geladen werden können:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>RenamePhotos</groupId>
	<artifactId>RenamePhotos</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<dependencies>
		<dependency>
			<groupId>com.drewnoakes</groupId>
			<artifactId>metadata-extractor</artifactId>
			<version>2.6.2</version>
		</dependency>
	</dependencies>
</project>

Hier die Dateien vorher:

Und nachher:

Die Ausgabe auf der Konsole sieht dazu so aus:

CIMG2773.JPG -> 2012-02-23_172829_CIMG2773.JPG
CIMG2774.JPG -> 2012-02-23_172842_CIMG2774.JPG
CIMG2775.JPG -> 2012-02-23_172847_CIMG2775.JPG
CIMG2776.JPG -> 2012-02-23_172853_CIMG2776.JPG
CIMG2777.JPG -> 2012-02-23_184732_CIMG2777.JPG
CIMG2778.JPG -> 2012-02-23_184746_CIMG2778.JPG
CIMG2779.JPG -> 2012-02-23_184752_CIMG2779.JPG
CIMG2780.JPG -> 2012-02-23_211635_CIMG2780.JPG
CIMG2781.JPG -> 2012-02-23_211646_CIMG2781.JPG
CIMG2782.JPG -> 2012-02-23_211655_CIMG2782.JPG
CIMG2783.JPG -> 2012-02-23_211714_CIMG2783.JPG
CIMG2784.JPG -> 2012-02-23_211724_CIMG2784.JPG
CIMG2785.JPG -> 2012-02-23_211735_CIMG2785.JPG
CIMG2786.JPG -> 2012-02-23_211745_CIMG2786.JPG
CIMG2787.JPG -> 2012-02-23_211801_CIMG2787.JPG
CIMG2788.JPG -> 2012-02-23_211810_CIMG2788.JPG
CIMG2789.JPG -> 2012-02-23_211822_CIMG2789.JPG
error parsing D:\PhotosToRename\desktop.ini

Der Code Style für den eclipse Java Formatter stammt von Google: https://google-styleguide.googlecode.com/svn/trunk/eclipse-java-google-style.xml

Ach ja, es gibt natürlich auch diverse kleine Tools, die das auch alles können, z.B. ExifRenamer für Mac OS oder dieses Tool für Windows.

Alte Backups unter Linux automatisch löschen

Dieses einfache Script geht davon aus, dass im Verzeichnis /var/data/my-ftp-backup-folder/ Backups vorliegen und fängt an, alte Dateien zu löschen, sobald mehr als 7 Dateien vorliegen. Die Dateien müssen dabei schon passend geordnet vorliegen, z.B.
backup1.zip
backup2.zip
backup3.zip

backup9.zip


#!/bin/bash
# remove old backups automatically
# Version 1.0 -  03.04.2014
# https://www.denniswilmsmann.de/2014/04/alte-backups-unter-linux-automatisch-loeschen/

# how many backups should be kept?
let maxcount=7

let counter=0

for backupfile in /var/data/my-ftp-backup-folder/*; do
 let counter=$counter+1
 echo $backupfile
done
echo "Backup count:" $counter
if test $counter -gt $maxcount;
 then
 echo "Too many backups, forcing deletion..."
 let newcounter=0
 for deletefile in /var/data/my-ftp-backup-folder/*; do
 let newcounter=$newcounter+1
 # echo "newcounter" $newcounter
 let temp=$counter-$maxcount
 # echo "temp" $temp
 if test $newcounter -le $temp;
 then
 rm $deletefile
 echo "Deleting file" $deletefile
 fi

done

else
 echo "Nothing to do."
fi

Falls Dateien gelöscht werden, wird der betroffene Dateiename ausgegeben. Falls 7 (oder weniger) Dateien vorliegen, wird die Meldung „Nothing to do.“ ausgegeben. Ich lasse das Script täglich als cronjob laufen, um zu vermeiden, dass mein Backupverzeichnis vollläuft.

Funktioniert zuverlässig. 🙂 In dem Beispiel oben mit den Backups von 1 bis 9 löscht das Script also backup1.zip und backup2.zip.

Spindown bei Festplatten verhindern

In meinem Notebook MSI GE60 ist eine Western Digital WD10JPVT Blue als Festplatte verbaut, die leider das tolle Feature besitzt, bereits nach ca. 3 Sekunden einen Spindown durchzuführen. Wenn danach dann wieder auf die Festplatte zugegriffen wird, gibts eine kurze Verzögerung, die nicht wirklich lange dauert, aber bemerkbar ist und nervt. Vom Hersteller Western Digitial gibt es für das Problem leider keine Lösung bzw. ich habe keine gefunden.

Mit ein paar Zeilen Java läßt sich das Problem umgehen: Es werden alle x Sekunden ein paar Zeichen Text in eine Datei geschrieben, die nach x Durchläufen gelöscht und neu erstellt wird. Das kostet kaum Performance, sorgt aber für minimale Last auf der Festplatte.

package de.denniswilmsmann.preventspindown;

import java.awt.AWTException;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

/*
 * $Log: $
 */

/**
 * <br/>Created: 18.04.2013
 * <br/>Author: Dennis Wilmsmann
 * <br/>Last committed $Date: $
 * @author  $Author: $
 * @version $Revision: $
 */
public class PreventSpindown {

    private static final String PREFIX = "preventspindown_";
    private static PreventSpinDownThread t;
    private static int secondsPerLine = 5;
    private static int linesPerFile = 10;
    private static TrayIcon icon;
    private static Image stopImage;
    private static Image runImage;
    private static String running = "Prevent Spindown - Running";
    private static String stopped = "Prevent Spindown - Stopped";

    /**
     * @param args String[]
     * @throws URISyntaxException
     * @throws IOException
     * @throws AWTException
     */
    public static void main(String[] args) throws URISyntaxException, IOException, AWTException {
        // check command line params
        if (args != null) {
            for (String arg : args) {
                if (arg.startsWith("secondsPerLine")) {
                    secondsPerLine = Integer.parseInt(arg.split("=")[1]);
                } else if (arg.startsWith("linesPerFile")) {
                    linesPerFile = Integer.parseInt(arg.split("=")[1]);
                }
            }
        }

        // at least 1 line per second and 1 line per file
        secondsPerLine = Math.max(1, secondsPerLine);
        linesPerFile = Math.max(1, linesPerFile);

        SystemTray tray = SystemTray.getSystemTray();
        Toolkit toolkit = Toolkit.getDefaultToolkit();

        stopImage = toolkit.getImage("drive.png");
        runImage = toolkit.getImage("drive-green.png");

        PopupMenu menu = new PopupMenu();

        MenuItem startItem = new MenuItem("Start");
        startItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (t == null || !t.isRunning()) {
                    t = new PreventSpinDownThread(secondsPerLine, linesPerFile);
                    t.start();
                    icon.setImage(runImage);
                    icon.setToolTip(running);
                }
            }
        });
        menu.add(startItem);

        MenuItem stopItem = new MenuItem("Stop");
        stopItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (t != null) {
                    t.setRunning(false);
                    icon.setImage(stopImage);
                    icon.setToolTip(stopped);
                }
            }
        });
        menu.add(stopItem);

        MenuItem closeItem = new MenuItem("Close");
        closeItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });
        menu.add(closeItem);
        icon = new TrayIcon(stopImage, stopped, menu);
        icon.setImageAutoSize(true);

        tray.add(icon);
    }

    public static class PreventSpinDownThread extends Thread {

        private final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HHmmss");
        private int secondsPerLine = 5;
        private int linesPerFile = 10;
        private boolean running = true;

        public void setRunning(boolean running) {
            this.running = running;
        }

        public PreventSpinDownThread(int secondsPerLine, int linesPerFile) {
            this.secondsPerLine = secondsPerLine;
            this.linesPerFile = linesPerFile;
        }

        @Override
        public void run() {
            // remove old files first
            File tmp = new File("");
            File folder = new File(tmp.getAbsolutePath());
            if (folder.isDirectory()) {
                for (File f : folder.listFiles()) {
                    if (f.getName().startsWith(PREFIX) && f.getName().contains("txt")) {
                        System.out.println("remove old file " + f.getAbsolutePath());
                        f.delete();
                    }

                }
            }

            System.out.println("write a new line each " + secondsPerLine + " seconds");
            System.out.println("write a new file each " + linesPerFile + " lines");

            int count = 0;
            File newTextFile = null;
            String filename = null;

            try {
                while (running) {
                    if (newTextFile == null) {
                        filename = PREFIX + df.format(new Date()) + ".txt";
                        newTextFile = new File(filename);
                        System.out.println("create new file " + newTextFile.getAbsolutePath());
                        newTextFile.deleteOnExit();
                    }
                    FileWriter fileWriter = new FileWriter(newTextFile, true);

                    String random = UUID.randomUUID().toString();
                    System.out.println(random);

                    fileWriter.write(random);
                    fileWriter.write("\n");
                    fileWriter.close();
                    count++;

                    icon.setToolTip(PreventSpindown.running + " " + count + "/" + linesPerFile + "\n" + filename);

                    Thread.sleep(1000 * secondsPerLine);

                    if (count >= linesPerFile) {
                        count = 0;
                        System.out.println("remove old file " + newTextFile.getAbsolutePath());
                        newTextFile.delete();
                        newTextFile = null;
                    }
                }
                // delete file after stop
                newTextFile.delete();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public boolean isRunning() {
            return running;
        }
    }
}

Update vom 18.04.2013: Die Konsole wird per default nicht mehr angezeigt und dafür gibts ein kleines Icon im Systray, um per Rechtsklick zu starten bzw. zu stoppen.



psd3

Ich habe mal alles zusammen in ein ausführbares JAR File gepackt mit dazugehöriger start.bat Datei für Windows. Das sollte eigentlich auf jedem System laufen, auf dem Java 6 (oder aktueller) installiert ist: preventspindown.zip

Das ZIP File irgendwo entpacken und via start.bat starten. Wie oft eine neue Zeile geschrieben werden soll und wann eine neue Datei erstellt wird, wird über die Parameter secondsPerLine (default=5) und linesPerFile (default=10) gesteuert. Einfach nach den eigenen Wünschen in der start.bat anpassen.

Wenn in der start.bat wieder mit java.exe anstatt javaw.exe gestartet wird, erscheint auch wieder die Konsole:

preventspindown

Meine Erfahrungen auf dem Weg zum SCJP

Als Buch kann ich SCJP Sun Certified Programmer for Java 6 Study Guide (Exam 310-065) von Sierra&Bates empfehlen, weitere Literatur habe ich nicht genutzt. Die Tests im Buch selbst und auf der beigelegten CD sind eigentlich ausreichend, wenn man sie denn nur oft genug macht. Von diversen (kostenpflichtigen) Online Exams habe ich die Finger gelassen und auch vom ExamLab, was auf der JavaRanch sehr häufig als das Tool zur Prüfungsvorbereitung empfohlen wird. Ich fand die Fragen viel zu schwer und viel zu abwegig, selbst im Vergleich zu den restlichen Fragen, die einem in Sachen SCJP so über den Weg laufen. Wenn man viel freie Zeit hat, sollten 6 bis 8 Wochen zur Vorbereitung ausreichen, denke ich. Bei mir waren es jetzt gute 6 Monate, nebenbei zum Berufsalltag. Die Prüfung selbst war einfacher als die Testfragen, was im Buch von Sierra&Bates ja auch sehr oft erwähnt wird. Was mir bei der Prüfung allerdings aufgefallen ist, ist die hohe Anzahl an Aufgaben, wo Codeschnipsel zusammengebastelt werden müssen. Dazu kamen subjektiv empfunden sehr viele Fragen zu den Themen Generics und Threads. Weniger im Fokus standen Garbage Collection und alles, was mit der Java Kommandozeile zu tun hat. Da die Zusammenstellung der Fragen aber eh zufällig ist, wird das wohl in jeder Prüfung auch anders aussehen. Zum Thema Zeit: 3 Stunden für die Prüfung sind mehr als ausreichend, selbst 2 Stunden sollten reichen.

Probleme mit DDMS?

Wer Probleme hat, per DDMS Befehle oder Werte an den Google Android Emulator zu übergeben und ein deutsches Windows XP benutzt, muss leider die "Regionalen Einstellungen" auf Englisch (USA) setzen.

Systemsteuerung, Regions- und Sprachoptionen