alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Java example source code file (SynthGraphicsUtils.java)

This example Java source code file (SynthGraphicsUtils.java) is included in the alvinalexander.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Learn more about this Java project at its project page.

Java - Java tags/keywords

awt, dimension, fontmetrics, graphics, gui, icon, insets, jcomponent, jmenuitem, object, rectangle, string, swing, synthcontext, synthiconwrapper, synthmenuitemlayouthelper, text, view

The SynthGraphicsUtils.java Java example source code

/*
 * Copyright (c) 2002, 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package javax.swing.plaf.synth;

import sun.swing.SwingUtilities2;
import sun.swing.MenuItemLayoutHelper;

import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.text.*;
import sun.swing.plaf.synth.*;

/**
 * Wrapper for primitive graphics calls.
 *
 * @since 1.5
 * @author Scott Violet
 */
public class SynthGraphicsUtils {
    // These are used in the text painting code to avoid allocating a bunch of
    // garbage.
    private Rectangle paintIconR = new Rectangle();
    private Rectangle paintTextR = new Rectangle();
    private Rectangle paintViewR = new Rectangle();
    private Insets paintInsets = new Insets(0, 0, 0, 0);

    // These Rectangles/Insets are used in the text size calculation to avoid a
    // a bunch of garbage.
    private Rectangle iconR = new Rectangle();
    private Rectangle textR = new Rectangle();
    private Rectangle viewR = new Rectangle();
    private Insets viewSizingInsets = new Insets(0, 0, 0, 0);

    /**
     * Creates a <code>SynthGraphicsUtils.
     */
    public SynthGraphicsUtils() {
    }

    /**
     * Draws a line between the two end points.
     *
     * @param context Identifies hosting region.
     * @param paintKey Identifies the portion of the component being asked
     *                 to paint, may be null.
     * @param g Graphics object to paint to
     * @param x1 x origin
     * @param y1 y origin
     * @param x2 x destination
     * @param y2 y destination
     */
    public void drawLine(SynthContext context, Object paintKey,
                         Graphics g, int x1, int y1, int x2, int y2) {
        g.drawLine(x1, y1, x2, y2);
    }

    /**
     * Draws a line between the two end points.
     * <p>This implementation supports only one line style key,
     * <code>"dashed". The "dashed" line style is applied
     * only to vertical and horizontal lines.
     * <p>Specifying null or any key different from
     * <code>"dashed" will draw solid lines.
     *
     * @param context identifies hosting region
     * @param paintKey identifies the portion of the component being asked
     *                 to paint, may be null
     * @param g Graphics object to paint to
     * @param x1 x origin
     * @param y1 y origin
     * @param x2 x destination
     * @param y2 y destination
     * @param styleKey identifies the requested style of the line (e.g. "dashed")
     * @since 1.6
     */
    public void drawLine(SynthContext context, Object paintKey,
                         Graphics g, int x1, int y1, int x2, int y2,
                         Object styleKey) {
        if ("dashed".equals(styleKey)) {
            // draw vertical line
            if (x1 == x2) {
                y1 += (y1 % 2);

                for (int y = y1; y <= y2; y+=2) {
                    g.drawLine(x1, y, x2, y);
                }
            // draw horizontal line
            } else if (y1 == y2) {
                x1 += (x1 % 2);

                for (int x = x1; x <= x2; x+=2) {
                    g.drawLine(x, y1, x, y2);
                }
            // oblique lines are not supported
            }
        } else {
            drawLine(context, paintKey, g, x1, y1, x2, y2);
        }
    }

    /**
     * Lays out text and an icon returning, by reference, the location to
     * place the icon and text.
     *
     * @param ss SynthContext
     * @param fm FontMetrics for the Font to use, this may be ignored
     * @param text Text to layout
     * @param icon Icon to layout
     * @param hAlign horizontal alignment
     * @param vAlign vertical alignment
     * @param hTextPosition horizontal text position
     * @param vTextPosition vertical text position
     * @param viewR Rectangle to layout text and icon in.
     * @param iconR Rectangle to place icon bounds in
     * @param textR Rectangle to place text in
     * @param iconTextGap gap between icon and text
     */
    public String layoutText(SynthContext ss, FontMetrics fm,
                         String text, Icon icon, int hAlign,
                         int vAlign, int hTextPosition,
                         int vTextPosition, Rectangle viewR,
                         Rectangle iconR, Rectangle textR, int iconTextGap) {
        if (icon instanceof SynthIcon) {
            SynthIconWrapper wrapper = SynthIconWrapper.get((SynthIcon)icon,
                                                            ss);
            String formattedText = SwingUtilities.layoutCompoundLabel(
                      ss.getComponent(), fm, text, wrapper, vAlign, hAlign,
                      vTextPosition, hTextPosition, viewR, iconR, textR,
                      iconTextGap);
            SynthIconWrapper.release(wrapper);
            return formattedText;
        }
        return SwingUtilities.layoutCompoundLabel(
                      ss.getComponent(), fm, text, icon, vAlign, hAlign,
                      vTextPosition, hTextPosition, viewR, iconR, textR,
                      iconTextGap);
    }

    /**
     * Returns the size of the passed in string.
     *
     * @param ss SynthContext
     * @param font Font to use
     * @param metrics FontMetrics, may be ignored
     * @param text Text to get size of.
     */
    public int computeStringWidth(SynthContext ss, Font font,
                                  FontMetrics metrics, String text) {
        return SwingUtilities2.stringWidth(ss.getComponent(), metrics,
                                          text);
    }

    /**
     * Returns the minimum size needed to properly render an icon and text.
     *
     * @param ss SynthContext
     * @param font Font to use
     * @param text Text to layout
     * @param icon Icon to layout
     * @param hAlign horizontal alignment
     * @param vAlign vertical alignment
     * @param hTextPosition horizontal text position
     * @param vTextPosition vertical text position
     * @param iconTextGap gap between icon and text
     * @param mnemonicIndex Index into text to render the mnemonic at, -1
     *        indicates no mnemonic.
     */
    public Dimension getMinimumSize(SynthContext ss, Font font, String text,
                      Icon icon, int hAlign, int vAlign, int hTextPosition,
                      int vTextPosition, int iconTextGap, int mnemonicIndex) {
        JComponent c = ss.getComponent();
        Dimension size = getPreferredSize(ss, font, text, icon, hAlign,
                                          vAlign, hTextPosition, vTextPosition,
                                          iconTextGap, mnemonicIndex);
        View v = (View) c.getClientProperty(BasicHTML.propertyKey);

        if (v != null) {
            size.width -= v.getPreferredSpan(View.X_AXIS) -
                          v.getMinimumSpan(View.X_AXIS);
        }
        return size;
    }

    /**
     * Returns the maximum size needed to properly render an icon and text.
     *
     * @param ss SynthContext
     * @param font Font to use
     * @param text Text to layout
     * @param icon Icon to layout
     * @param hAlign horizontal alignment
     * @param vAlign vertical alignment
     * @param hTextPosition horizontal text position
     * @param vTextPosition vertical text position
     * @param iconTextGap gap between icon and text
     * @param mnemonicIndex Index into text to render the mnemonic at, -1
     *        indicates no mnemonic.
     */
    public Dimension getMaximumSize(SynthContext ss, Font font, String text,
                      Icon icon, int hAlign, int vAlign, int hTextPosition,
                      int vTextPosition, int iconTextGap, int mnemonicIndex) {
        JComponent c = ss.getComponent();
        Dimension size = getPreferredSize(ss, font, text, icon, hAlign,
                                          vAlign, hTextPosition, vTextPosition,
                                          iconTextGap, mnemonicIndex);
        View v = (View) c.getClientProperty(BasicHTML.propertyKey);

        if (v != null) {
            size.width += v.getMaximumSpan(View.X_AXIS) -
                          v.getPreferredSpan(View.X_AXIS);
        }
        return size;
    }

    /**
     * Returns the maximum height of the the Font from the passed in
     * SynthContext.
     *
     * @param context SynthContext used to determine font.
     * @return maximum height of the characters for the font from the passed
     *         in context.
     */
    public int getMaximumCharHeight(SynthContext context) {
        FontMetrics fm = context.getComponent().getFontMetrics(
            context.getStyle().getFont(context));
        return (fm.getAscent() + fm.getDescent());
    }

    /**
     * Returns the preferred size needed to properly render an icon and text.
     *
     * @param ss SynthContext
     * @param font Font to use
     * @param text Text to layout
     * @param icon Icon to layout
     * @param hAlign horizontal alignment
     * @param vAlign vertical alignment
     * @param hTextPosition horizontal text position
     * @param vTextPosition vertical text position
     * @param iconTextGap gap between icon and text
     * @param mnemonicIndex Index into text to render the mnemonic at, -1
     *        indicates no mnemonic.
     */
    public Dimension getPreferredSize(SynthContext ss, Font font, String text,
                      Icon icon, int hAlign, int vAlign, int hTextPosition,
                      int vTextPosition, int iconTextGap, int mnemonicIndex) {
        JComponent c = ss.getComponent();
        Insets insets = c.getInsets(viewSizingInsets);
        int dx = insets.left + insets.right;
        int dy = insets.top + insets.bottom;

        if (icon == null && (text == null || font == null)) {
            return new Dimension(dx, dy);
        }
        else if ((text == null) || ((icon != null) && (font == null))) {
            return new Dimension(SynthIcon.getIconWidth(icon, ss) + dx,
                                 SynthIcon.getIconHeight(icon, ss) + dy);
        }
        else {
            FontMetrics fm = c.getFontMetrics(font);

            iconR.x = iconR.y = iconR.width = iconR.height = 0;
            textR.x = textR.y = textR.width = textR.height = 0;
            viewR.x = dx;
            viewR.y = dy;
            viewR.width = viewR.height = Short.MAX_VALUE;

            layoutText(ss, fm, text, icon, hAlign, vAlign,
                   hTextPosition, vTextPosition, viewR, iconR, textR,
                   iconTextGap);
            int x1 = Math.min(iconR.x, textR.x);
            int x2 = Math.max(iconR.x + iconR.width, textR.x + textR.width);
            int y1 = Math.min(iconR.y, textR.y);
            int y2 = Math.max(iconR.y + iconR.height, textR.y + textR.height);
            Dimension rv = new Dimension(x2 - x1, y2 - y1);

            rv.width += dx;
            rv.height += dy;
            return rv;
        }
    }

    /**
     * Paints text at the specified location. This will not attempt to
     * render the text as html nor will it offset by the insets of the
     * component.
     *
     * @param ss SynthContext
     * @param g Graphics used to render string in.
     * @param text Text to render
     * @param bounds Bounds of the text to be drawn.
     * @param mnemonicIndex Index to draw string at.
     */
    public void paintText(SynthContext ss, Graphics g, String text,
                          Rectangle bounds, int mnemonicIndex) {
        paintText(ss, g, text, bounds.x, bounds.y, mnemonicIndex);
    }

    /**
     * Paints text at the specified location. This will not attempt to
     * render the text as html nor will it offset by the insets of the
     * component.
     *
     * @param ss SynthContext
     * @param g Graphics used to render string in.
     * @param text Text to render
     * @param x X location to draw text at.
     * @param y Upper left corner to draw text at.
     * @param mnemonicIndex Index to draw string at.
     */
    public void paintText(SynthContext ss, Graphics g, String text,
                          int x, int y, int mnemonicIndex) {
        if (text != null) {
            JComponent c = ss.getComponent();
            FontMetrics fm = SwingUtilities2.getFontMetrics(c, g);
            y += fm.getAscent();
            SwingUtilities2.drawStringUnderlineCharAt(c, g, text,
                                                      mnemonicIndex, x, y);
        }
    }

    /**
     * Paints an icon and text. This will render the text as html, if
     * necessary, and offset the location by the insets of the component.
     *
     * @param ss SynthContext
     * @param g Graphics to render string and icon into
     * @param text Text to layout
     * @param icon Icon to layout
     * @param hAlign horizontal alignment
     * @param vAlign vertical alignment
     * @param hTextPosition horizontal text position
     * @param vTextPosition vertical text position
     * @param iconTextGap gap between icon and text
     * @param mnemonicIndex Index into text to render the mnemonic at, -1
     *        indicates no mnemonic.
     * @param textOffset Amount to offset the text when painting
     */
    public void paintText(SynthContext ss, Graphics g, String text,
                      Icon icon, int hAlign, int vAlign, int hTextPosition,
                      int vTextPosition, int iconTextGap, int mnemonicIndex,
                      int textOffset) {
        if ((icon == null) && (text == null)) {
            return;
        }
        JComponent c = ss.getComponent();
        FontMetrics fm = SwingUtilities2.getFontMetrics(c, g);
        Insets insets = SynthLookAndFeel.getPaintingInsets(ss, paintInsets);

        paintViewR.x = insets.left;
        paintViewR.y = insets.top;
        paintViewR.width = c.getWidth() - (insets.left + insets.right);
        paintViewR.height = c.getHeight() - (insets.top + insets.bottom);

        paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0;
        paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0;

        String clippedText =
            layoutText(ss, fm, text, icon, hAlign, vAlign,
                   hTextPosition, vTextPosition, paintViewR, paintIconR,
                   paintTextR, iconTextGap);

        if (icon != null) {
            Color color = g.getColor();

            if (ss.getStyle().getBoolean(ss, "TableHeader.alignSorterArrow", false) &&
                "TableHeader.renderer".equals(c.getName())) {
                paintIconR.x = paintViewR.width - paintIconR.width;
            } else {
                paintIconR.x += textOffset;
            }
            paintIconR.y += textOffset;
            SynthIcon.paintIcon(icon, ss, g, paintIconR.x, paintIconR.y,
                                paintIconR.width, paintIconR.height);
            g.setColor(color);
        }

        if (text != null) {
            View v = (View) c.getClientProperty(BasicHTML.propertyKey);

            if (v != null) {
                v.paint(g, paintTextR);
            } else {
                paintTextR.x += textOffset;
                paintTextR.y += textOffset;

                paintText(ss, g, clippedText, paintTextR, mnemonicIndex);
            }
        }
    }


     /**
      * A quick note about how preferred sizes are calculated... Generally
      * speaking, SynthPopupMenuUI will run through the list of its children
      * (from top to bottom) and ask each for its preferred size.  Each menu
      * item will add up the max width of each element (icons, text,
      * accelerator spacing, accelerator text or arrow icon) encountered thus
      * far, so by the time all menu items have been calculated, we will
      * know the maximum (preferred) menu item size for that popup menu.
      * Later when it comes time to paint each menu item, we can use those
      * same accumulated max element sizes in order to layout the item.
      */
    static Dimension getPreferredMenuItemSize(SynthContext context,
           SynthContext accContext, JComponent c,
           Icon checkIcon, Icon arrowIcon, int defaultTextIconGap,
           String acceleratorDelimiter, boolean useCheckAndArrow,
           String propertyPrefix) {

         JMenuItem mi = (JMenuItem) c;
         SynthMenuItemLayoutHelper lh = new SynthMenuItemLayoutHelper(
                 context, accContext, mi, checkIcon, arrowIcon,
                 MenuItemLayoutHelper.createMaxRect(), defaultTextIconGap,
                 acceleratorDelimiter, SynthLookAndFeel.isLeftToRight(mi),
                 useCheckAndArrow, propertyPrefix);

         Dimension result = new Dimension();

         // Calculate the result width
         int gap = lh.getGap();
         result.width = 0;
         MenuItemLayoutHelper.addMaxWidth(lh.getCheckSize(), gap, result);
         MenuItemLayoutHelper.addMaxWidth(lh.getLabelSize(), gap, result);
         MenuItemLayoutHelper.addWidth(lh.getMaxAccOrArrowWidth(), 5 * gap, result);
         // The last gap is unnecessary
         result.width -= gap;

         // Calculate the result height
         result.height = MenuItemLayoutHelper.max(lh.getCheckSize().getHeight(),
                 lh.getLabelSize().getHeight(), lh.getAccSize().getHeight(),
                 lh.getArrowSize().getHeight());

         // Take into account menu item insets
         Insets insets = lh.getMenuItem().getInsets();
         if (insets != null) {
             result.width += insets.left + insets.right;
             result.height += insets.top + insets.bottom;
         }

         // if the width is even, bump it up one. This is critical
         // for the focus dash lhne to draw properly
         if (result.width % 2 == 0) {
             result.width++;
         }

         // if the height is even, bump it up one. This is critical
         // for the text to center properly
         if (result.height % 2 == 0) {
             result.height++;
         }

         return result;
     }

    static void applyInsets(Rectangle rect, Insets insets, boolean leftToRight) {
        if (insets != null) {
            rect.x += (leftToRight ? insets.left : insets.right);
            rect.y += insets.top;
            rect.width -= (leftToRight ? insets.right : insets.left) + rect.x;
            rect.height -= (insets.bottom + rect.y);
        }
    }

    static void paint(SynthContext context, SynthContext accContext, Graphics g,
               Icon checkIcon, Icon arrowIcon, String acceleratorDelimiter,
               int defaultTextIconGap, String propertyPrefix) {
        JMenuItem mi = (JMenuItem) context.getComponent();
        SynthStyle style = context.getStyle();
        g.setFont(style.getFont(context));

        Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight());
        boolean leftToRight = SynthLookAndFeel.isLeftToRight(mi);
        applyInsets(viewRect, mi.getInsets(), leftToRight);

        SynthMenuItemLayoutHelper lh = new SynthMenuItemLayoutHelper(
                context, accContext, mi, checkIcon, arrowIcon, viewRect,
                defaultTextIconGap, acceleratorDelimiter, leftToRight,
                MenuItemLayoutHelper.useCheckAndArrow(mi), propertyPrefix);
        MenuItemLayoutHelper.LayoutResult lr = lh.layoutMenuItem();

        paintMenuItem(g, lh, lr);
    }

    static void paintMenuItem(Graphics g, SynthMenuItemLayoutHelper lh,
                              MenuItemLayoutHelper.LayoutResult lr) {
        // Save original graphics font and color
        Font holdf = g.getFont();
        Color holdc = g.getColor();

        paintCheckIcon(g, lh, lr);
        paintIcon(g, lh, lr);
        paintText(g, lh, lr);
        paintAccText(g, lh, lr);
        paintArrowIcon(g, lh, lr);

        // Restore original graphics font and color
        g.setColor(holdc);
        g.setFont(holdf);
    }

    static void paintBackground(Graphics g, SynthMenuItemLayoutHelper lh) {
        paintBackground(lh.getContext(), g, lh.getMenuItem());
    }

    static void paintBackground(SynthContext context, Graphics g, JComponent c) {
        context.getPainter().paintMenuItemBackground(context, g, 0, 0,
                c.getWidth(), c.getHeight());
    }

    static void paintIcon(Graphics g, SynthMenuItemLayoutHelper lh,
                          MenuItemLayoutHelper.LayoutResult lr) {
        if (lh.getIcon() != null) {
            Icon icon;
            JMenuItem mi = lh.getMenuItem();
            ButtonModel model = mi.getModel();
            if (!model.isEnabled()) {
                icon = mi.getDisabledIcon();
            } else if (model.isPressed() && model.isArmed()) {
                icon = mi.getPressedIcon();
                if (icon == null) {
                    // Use default icon
                    icon = mi.getIcon();
                }
            } else {
                icon = mi.getIcon();
            }

            if (icon != null) {
                Rectangle iconRect = lr.getIconRect();
                SynthIcon.paintIcon(icon, lh.getContext(), g, iconRect.x,
                        iconRect.y, iconRect.width, iconRect.height);
            }
        }
    }

    static void paintCheckIcon(Graphics g, SynthMenuItemLayoutHelper lh,
                               MenuItemLayoutHelper.LayoutResult lr) {
        if (lh.getCheckIcon() != null) {
            Rectangle checkRect = lr.getCheckRect();
            SynthIcon.paintIcon(lh.getCheckIcon(), lh.getContext(), g,
                    checkRect.x, checkRect.y, checkRect.width, checkRect.height);
        }
    }

    static void paintAccText(Graphics g, SynthMenuItemLayoutHelper lh,
                             MenuItemLayoutHelper.LayoutResult lr) {
        String accText = lh.getAccText();
        if (accText != null && !accText.equals("")) {
            g.setColor(lh.getAccStyle().getColor(lh.getAccContext(),
                    ColorType.TEXT_FOREGROUND));
            g.setFont(lh.getAccStyle().getFont(lh.getAccContext()));
            lh.getAccGraphicsUtils().paintText(lh.getAccContext(), g, accText,
                    lr.getAccRect().x, lr.getAccRect().y, -1);
        }
    }

    static void paintText(Graphics g, SynthMenuItemLayoutHelper lh,
                          MenuItemLayoutHelper.LayoutResult lr) {
        if (!lh.getText().equals("")) {
            if (lh.getHtmlView() != null) {
                // Text is HTML
                lh.getHtmlView().paint(g, lr.getTextRect());
            } else {
                // Text isn't HTML
                g.setColor(lh.getStyle().getColor(
                        lh.getContext(), ColorType.TEXT_FOREGROUND));
                g.setFont(lh.getStyle().getFont(lh.getContext()));
                lh.getGraphicsUtils().paintText(lh.getContext(), g, lh.getText(),
                        lr.getTextRect().x, lr.getTextRect().y,
                        lh.getMenuItem().getDisplayedMnemonicIndex());
            }
        }
    }

    static void paintArrowIcon(Graphics g, SynthMenuItemLayoutHelper lh,
                               MenuItemLayoutHelper.LayoutResult lr) {
        if (lh.getArrowIcon() != null) {
            Rectangle arrowRect = lr.getArrowRect();
            SynthIcon.paintIcon(lh.getArrowIcon(), lh.getContext(), g,
                    arrowRect.x, arrowRect.y, arrowRect.width, arrowRect.height);
        }
    }

    /**
     * Wraps a SynthIcon around the Icon interface, forwarding calls to
     * the SynthIcon with a given SynthContext.
     */
    private static class SynthIconWrapper implements Icon {
        private static final java.util.List<SynthIconWrapper> CACHE = new java.util.ArrayList(1);

        private SynthIcon synthIcon;
        private SynthContext context;

        static SynthIconWrapper get(SynthIcon icon, SynthContext context) {
            synchronized(CACHE) {
                int size = CACHE.size();
                if (size > 0) {
                    SynthIconWrapper wrapper = CACHE.remove(size - 1);
                    wrapper.reset(icon, context);
                    return wrapper;
                }
            }
            return new SynthIconWrapper(icon, context);
        }

        static void release(SynthIconWrapper wrapper) {
            wrapper.reset(null, null);
            synchronized(CACHE) {
                CACHE.add(wrapper);
            }
        }

        SynthIconWrapper(SynthIcon icon, SynthContext context) {
            reset(icon, context);
        }

        void reset(SynthIcon icon, SynthContext context) {
            synthIcon = icon;
            this.context = context;
        }

        public void paintIcon(Component c, Graphics g, int x, int y) {
            // This is a noop as this should only be for sizing calls.
        }

        public int getIconWidth() {
            return synthIcon.getIconWidth(context);
        }

        public int getIconHeight() {
            return synthIcon.getIconHeight(context);
        }
    }
}

Other Java examples (source code examples)

Here is a short list of links related to this Java SynthGraphicsUtils.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.