org.apache.wicket.extensions.markup.html.captcha.CaptchaImageResource.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.extensions.markup.html.captcha.CaptchaImageResource.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.wicket.extensions.markup.html.captcha;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.lang.ref.SoftReference;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.resource.DynamicImageResource;
import org.apache.wicket.util.io.IClusterable;
import org.apache.wicket.util.time.Time;

/**
 * Generates a captcha image.
 *
 * @author Joshua Perlow
 */
public class CaptchaImageResource extends DynamicImageResource {
    /**
     * This class is used to encapsulate all the filters that a character will get when rendered.
     * The changes are kept so that the size of the shapes can be properly recorded and reproduced
     * later, since it dynamically generates the size of the captcha image. The reason I did it this
     * way is because none of the JFC graphics classes are serializable, so they cannot be instance
     * variables here.
     */
    private static final class CharAttributes implements IClusterable {
        private static final long serialVersionUID = 1L;
        private final char c;
        private final String name;
        private final int rise;
        private final double rotation;
        private final double shearX;
        private final double shearY;

        CharAttributes(final char c, final String name, final double rotation, final int rise, final double shearX,
                final double shearY) {
            this.c = c;
            this.name = name;
            this.rotation = rotation;
            this.rise = rise;
            this.shearX = shearX;
            this.shearY = shearY;
        }

        char getChar() {
            return c;
        }

        String getName() {
            return name;
        }

        int getRise() {
            return rise;
        }

        double getRotation() {
            return rotation;
        }

        double getShearX() {
            return shearX;
        }

        double getShearY() {
            return shearY;
        }
    }

    private static final long serialVersionUID = 1L;

    private static int randomInt(final Random rng, final int min, final int max) {
        return (int) (rng.nextDouble() * (max - min) + min);
    }

    private static String randomString(final Random rng, final int min, final int max) {
        int num = randomInt(rng, min, max);
        byte b[] = new byte[num];
        for (int i = 0; i < num; i++) {
            b[i] = (byte) randomInt(rng, 'a', 'z');
        }
        return new String(b);
    }

    private static final RandomNumberGeneratorFactory RNG_FACTORY = new RandomNumberGeneratorFactory();

    private final IModel<String> challengeId;

    private final List<String> fontNames = Arrays.asList("Helvetica", "Arial", "Courier");
    private final int fontSize;
    private final int fontStyle;

    /**
     * Transient image data so that image only needs to be re-generated after de-serialization
     */
    private transient SoftReference<byte[]> imageData;

    private final int margin;
    private final Random rng;

    /**
     * Construct.
     */
    public CaptchaImageResource() {
        this(randomString(RNG_FACTORY.newRandomNumberGenerator(), 10, 14));
    }

    /**
     * Construct.
     *
     * @param challengeId
     *          The id of the challenge
     */
    public CaptchaImageResource(final String challengeId) {
        this(Model.of(challengeId));
    }

    /**
     * Construct.
     *
     * @param challengeId
     *          The id of the challenge
     */
    public CaptchaImageResource(final IModel<String> challengeId) {
        this(challengeId, 48, 30);
    }

    /**
     * Construct.
     *
     * @param challengeId
     *          The id of the challenge
     * @param fontSize
     *          The font size
     * @param margin
     *          The image's margin
     */
    public CaptchaImageResource(final IModel<String> challengeId, final int fontSize, final int margin) {
        this.challengeId = challengeId;
        this.fontStyle = 1;
        this.fontSize = fontSize;
        this.margin = margin;
        this.rng = newRandomNumberGenerator();
    }

    /**
     * Construct.
     *
     * @param challengeId
     *          The id of the challenge
     * @param fontSize
     *          The font size
     * @param margin
     *          The image's margin
     */
    public CaptchaImageResource(final String challengeId, final int fontSize, final int margin) {
        this(Model.of(challengeId), fontSize, margin);
    }

    protected Random newRandomNumberGenerator() {
        return RNG_FACTORY.newRandomNumberGenerator();
    }

    /**
     * Gets the id for the challenge.
     *
     * @return The id for the challenge
     */
    public final String getChallengeId() {
        return challengeId.getObject();
    }

    /**
     * Gets the id for the challenge
     *
     * @return The id for the challenge
     */
    public final IModel<String> getChallengeIdModel() {
        return challengeId;
    }

    /**
     * Causes the image to be redrawn the next time its requested.
     */
    public final void invalidate() {
        imageData = null;
    }

    @Override
    protected final byte[] getImageData(final Attributes attributes) {
        // get image data is always called in sync block
        byte[] data = null;
        if (imageData != null) {
            data = imageData.get();
        }
        if (data == null) {
            data = render();
            imageData = new SoftReference<>(data);
            setLastModifiedTime(Time.now());
        }
        return data;
    }

    private Font getFont(final String fontName) {
        return new Font(fontName, fontStyle, fontSize);
    }

    /**
     * Renders this image
     *
     * @return The image data
     */
    protected byte[] render() {
        int width = margin * 2;
        int height = margin * 2;
        char[] chars = challengeId.getObject().toCharArray();
        List<CharAttributes> charAttsList = new ArrayList<>();
        TextLayout text;
        AffineTransform textAt;
        Shape shape;

        for (char ch : chars) {
            String fontName = fontNames.get(randomInt(rng, 0, fontNames.size()));
            double rotation = Math.toRadians(randomInt(rng, -35, 35));
            int rise = randomInt(rng, margin / 2, margin);

            double shearX = rng.nextDouble() * 0.2;
            double shearY = rng.nextDouble() * 0.2;
            CharAttributes cf = new CharAttributes(ch, fontName, rotation, rise, shearX, shearY);
            charAttsList.add(cf);
            text = new TextLayout(ch + "", getFont(fontName), new FontRenderContext(null, false, false));
            textAt = new AffineTransform();
            textAt.rotate(rotation);
            textAt.shear(shearX, shearY);
            shape = text.getOutline(textAt);
            width += (int) shape.getBounds2D().getWidth();
            if (height < (int) shape.getBounds2D().getHeight() + rise) {
                height = (int) shape.getBounds2D().getHeight() + rise;
            }
        }

        final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D gfx = (Graphics2D) image.getGraphics();
        gfx.setBackground(Color.WHITE);
        int curWidth = margin;
        for (CharAttributes cf : charAttsList) {
            text = new TextLayout(cf.getChar() + "", getFont(cf.getName()), gfx.getFontRenderContext());
            textAt = new AffineTransform();
            textAt.translate(curWidth, height - cf.getRise());
            textAt.rotate(cf.getRotation());
            textAt.shear(cf.getShearX(), cf.getShearY());
            shape = text.getOutline(textAt);
            curWidth += shape.getBounds().getWidth();
            gfx.setXORMode(Color.BLACK);
            gfx.fill(shape);
        }

        // XOR circle
        int dx = randomInt(rng, width, 2 * width);
        int dy = randomInt(rng, width, 2 * height);
        int x = randomInt(rng, 0, width / 2);
        int y = randomInt(rng, 0, height / 2);

        gfx.setXORMode(Color.BLACK);
        gfx.setStroke(new BasicStroke(randomInt(rng, fontSize / 8, fontSize / 2)));
        gfx.drawOval(x, y, dx, dy);

        WritableRaster rstr = image.getRaster();
        int[] vColor = new int[3];
        int[] oldColor = new int[3];

        // noise
        for (x = 0; x < width; x++) {
            for (y = 0; y < height; y++) {
                rstr.getPixel(x, y, oldColor);

                // hard noise
                vColor[0] = (int) (Math.floor(rng.nextFloat() * 1.03) * 255);
                // soft noise
                vColor[0] = vColor[0] ^ (170 + (int) (rng.nextFloat() * 80));
                // xor to image
                vColor[0] = vColor[0] ^ oldColor[0];
                vColor[1] = vColor[0];
                vColor[2] = vColor[0];

                rstr.setPixel(x, y, vColor);
            }
        }
        return toImageData(image);
    }

    /**
     * The {@code RandomNumberGeneratorFactory} uses {@link java.security.SecureRandom} as RNG and {@code NativePRNG}
     * on unix and {@code Windows-PRNG} on windows if it exists. Else it will fallback to {@code SHA1PRNG}.
     * <p/>
     * Please keep in mind that {@link java.security.SecureRandom} uses{@code /dev/random} as default on unix systems
     * which is a blocking call. It is possible to change this by adding {@code -Djava.security.egd=file:/dev/urandom}
     * to your application server startup script.
     */
    private static final class RandomNumberGeneratorFactory {
        private final Provider.Service service;

        RandomNumberGeneratorFactory() {
            this.service = detectBestFittingService();
        }

        /**
         * Checks all existing security providers and returns the best fitting service.
         *
         * This method is different to {@link java.security.SecureRandom#getPrngAlgorithm()} which uses the first PRNG
         * algorithm of the first provider that has registered a SecureRandom implementation.
         * {@code detectBestFittingService()} instead uses a native PRNG if available, then
         * {@code SHA1PRNG} else {@code null} which triggers {@link java.security.SecureRandom#getPrngAlgorithm()}
         * when calling {@code new SecureRandom()}.
         *
         * @return a native pseudo random number generator or sha1 as fallback.
         */
        private Provider.Service detectBestFittingService() {
            Provider.Service _sha1Service = null;

            for (Provider provider : Security.getProviders()) {
                for (Provider.Service service : provider.getServices()) {
                    if ("SecureRandom".equals(service.getType())) {
                        String algorithm = service.getAlgorithm();
                        if ("NativePRNG".equals(algorithm)) {
                            return service;
                        } else if ("Windows-PRNG".equals(algorithm)) {
                            return service;
                        } else if (_sha1Service == null && "SHA1PRNG".equals(algorithm)) {
                            _sha1Service = service;
                        }
                    }
                }
            }

            return _sha1Service;
        }

        /**
         * @return new secure random number generator instance using best fitting service
         */
        Random newRandomNumberGenerator() {
            if (service != null) {
                try {
                    return SecureRandom.getInstance(service.getAlgorithm(), service.getProvider());
                } catch (NoSuchAlgorithmException nsax) {
                    // this shouldn't happen, because 'detectBestFittingService' has checked for existing provider and
                    // algorithms.
                }
            }

            return new SecureRandom();
        }
    }
}