10

For multiple image retrieval I am calling a PhotoHelperServlet with an anchor tag to get imageNames(multiple images) as follows

PhotoHelperServlet to get names of Images

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// Getting userid from session

Image image = new Image();
image.setUserid(userid);

ImageDAO imageDAO = new ImageDAO();

try {

    List<Image> imageId = imageDAO.listNames(image);

    if (imageId == null) { 
        // check if imageId is retreived
    }

    request.setAttribute("imageId", imageId);

    //Redirect it to home page
    RequestDispatcher rd = request.getRequestDispatcher("/webplugin/jsp/profile/photos.jsp");
    rd.forward(request, response);

catch (Exception e) {
    e.printStackTrace();
}

In ImageDAO listNames() method :

public List<Image> listNames(Image image) throws IllegalArgumentException, SQLException, ClassNotFoundException {

    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultset = null;
    Database database = new Database();
    List<Image> imageId = new ArrayList<Image>();

    try {

        connection = database.openConnection();
        preparedStatement = connection.prepareStatement(SQL_GET_PHOTOID);                  
        preparedStatement.setLong(1, image.getUserid());
        resultset = preparedStatement.executeQuery();

        while(resultset.next()) {
            image.setPhotoid(resultset.getLong(1));
            imageId.add(image);
        }

    } catch (SQLException e) {
        throw new SQLException(e);
    } finally {
        close(connection, preparedStatement, resultset);
    }
    return imageId;
}

In JSP code:

<c:forEach items="${imageId}" var="imageid">
    <img src="Photos/${imageid}">
</c:forEach>

In PhotoServlet doGet() method to get a photo:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

String imageid = request.getPathInfo().substring(1);

if(imageid == null) {
    // check for null and response.senderror
}

ImageDAO imageDAO = new ImageDAO();

try {

    Image image = imageDAO.getPhotos(imageid);

    if(image == null) {}

    BufferedInputStream input = null;
    BufferedOutputStream output = null;

    try {

        input = new BufferedInputStream(image.getPhoto(), DEFAULT_BUFFER_SIZE);
        output = new BufferedOutputStream(response.getOutputStream(), DEFAULT_BUFFER_SIZE);

        // Write file contents to response.
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int length;
        while ((length = input.read(buffer)) > 0) {
            output.write(buffer, 0, length);
        }
    } finally {
        if (output != null) try { output.close(); } catch (IOException logOrIgnore) {}
        if (input != null) try { input.close(); } catch (IOException logOrIgnore) {}
    }

} catch(Exception e) {
    e.printStackTrace();
}

In ImageDAO getPhotos() method

public Image getPhotos(String imageid) throws IllegalArgumentException, SQLException, ClassNotFoundException {

    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultset = null;
    Database database = new Database();
    Image image = new Image();

    try {

        connection = database.openConnection();
        preparedStatement = connection.prepareStatement(SQL_GET_PHOTO);                  
        preparedStatement.setString(1, imageid);
        resultset = preparedStatement.executeQuery();

        while(resultset.next()) {
            image.setPhoto(resultset.getBinaryStream(1));
        }

    } catch (SQLException e) {
        throw new SQLException(e);
    } finally {
        close(connection, preparedStatement, resultset);
    }
    return image;
}

In web.xml

<!-- Getting each photo -->
<servlet>
    <servlet-name>Photos Module</servlet-name>
    <servlet-class>app.controllers.PhotoServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Photos Module</servlet-name>
    <url-pattern>/Photos/*</url-pattern>
</servlet-mapping>

<!-- Getting photo names -->
<servlet>
    <servlet-name>Photo Module</servlet-name>
    <servlet-class>app.controllers.PhotoHelperServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Photo Module</servlet-name>
    <url-pattern>/Photo</url-pattern>
</servlet-mapping>

Question:

I am getting following Exception:

java.io.IOException: Stream closed

on this Line:

at app.controllers.PhotoServlet.doGet(PhotoServlet.java:94)
while ((length = input.read(buffer)) > 0) {

The full Exception:

java.io.IOException: Stream closed
at java.io.BufferedInputStream.getInIfOpen(BufferedInputStream.java:134)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:256)
at java.io.BufferedInputStream.read(BufferedInputStream.java:317)
at java.io.FilterInputStream.read(FilterInputStream.java:90)
at app.controllers.PhotoServlet.doGet(PhotoServlet.java:94)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:304)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:240)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:164)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:498)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:164)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:562)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:394)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:243)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:188)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:166)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:302)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Tom11
  • 2,419
  • 8
  • 30
  • 56
a k
  • 797
  • 8
  • 15
  • 24
  • Please post the code in image.getPhoto(), because that is where the problem most likely is. – M Platvoet Jun 19 '11 at 20:02
  • 1
    `post the code` means? do you think `image.getPhoto() has not got any photos(value) in it? – a k Jun 19 '11 at 20:11
  • I think, as mentioned in my previous answer, you actually are closing the underlying stream in getPhoto(). So that you essentially feeding the BufferedStream a source that already has been closed. – M Platvoet Jun 19 '11 at 20:14

3 Answers3

7

I'd imagine that the basic code flow is laid out like follows:

try {
    Get connection, statement, resultset
    Use connection, statement, resultset
    Get inputstream of resultset
} finally {
    Close resultset, statement, connection
}

try {
    Get outputstream
    Use inputstream of resultset, outputstream
} finally {
    Close outputstream, inputstream of resultset
}

And that the close of the ResultSet has implicitly closed the InputStream. It look like that your JDBC driver does not store the InputStream of the ResultSet fully in memory or on temp storage when the ResultSet is closed. Perhaps the JDBC driver is a bit simplistic, or not well thought designed, or the image is too large to be stored in memory. Who knows.

I'd first figure out what JDBC driver impl/version you're using and then consult its developer documentation to learn about settings which may be able to change/fix this behaviour. If you still can't figure it out, then you'd have to rearrange the basic code flow as follows:

try {
    Get connection, statement, resultset
    Use connection, statement, resultset
    try {
        Get inputstream of resultset, outputstream
        Use inputstream of resultset, outputstream
    } finally {
        Close outputstream, inputstream of resultset
    }
} finally {
    Close resultset, statement, connection
}

Or

try {
    Get connection, statement, resultset
    Use connection, statement, resultset
    Get inputstream of resultset
    Copy inputstream of resultset
} finally {
    Close resultset, statement, connection
}

try {
    Get outputstream
    Use copy of inputstream, outputstream
} finally {
    Close outputstream, copy of inputstream
}

The first approach is the most efficient, only the code is clumsy. The second approach is memory inefficient when you're copying to ByteArrayOutputStream, or performance inefficient when you're copying to FileOutputStream. If the images are mostly small and do not exceed a megabyte or something, then I'd just copy it to ByteArrayOutputStream.

InputStream input = null;
OutputStream output = null;

try {
    input = new BufferedInputStream(resultSet.getBinaryStream("columnName"), DEFAULT_BUFFER_SIZE);
    output = new ByteArrayOutputStream();
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

    for (int length; ((length = input.read(buffer)) > 0;) {
        output.write(buffer, 0, length);
    }
} finally {
    if (output != null) try { output.close(); } catch (IOException ignore) {}
    if (input != null) try { input.close(); } catch (IOException ignore) {}
}

Image image = new Image();
image.setPhoto(new ByteArrayInputStream(output.toByteArray()));
// ...
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • I have updated my full code including DAO methods to get `listNames() method` and `getPhotos()` to get `names of images` and `single image`. I am using `MySQL jdbc driver 5.1` – a k Jun 20 '11 at 14:04
  • Yes.. Did you try my suggestions? – BalusC Jun 20 '11 at 14:08
  • I am going through it and correcting as you suggested and will let you know if I `get/don't get` result. – a k Jun 20 '11 at 14:10
2
    参考一下:
ImageLoad
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.ImageSwitcher;
import android.widget.ImageView;

/**
 * 图片加载帮助类(自动异步加载、图片文件缓存、缓存文件管理)
 * 
 * @author n.zhang
 * 
 */
public class ImageLoad {
    private static final String TAG = "imageLoad";// 日志标签
    private static final String TAG_REF = TAG + "Ref";
    private Executor executor; // 线程池

    private int defaultImageID;// 默认图片id
    private Context context;// 你懂的
    private HashMap<String, PathInfo> cache = new HashMap<String, PathInfo>();// URL
    boolean sdCardExist = Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED); // 路径信息对应表
    private LinkedList<PathInfo> use = new LinkedList<PathInfo>();// 已在使用的路径信息队列
    private LinkedList<PathInfo> lost = new LinkedList<PathInfo>();// 还未使用的路径信息队列
    private LinkedList<PathInfo> original = new LinkedList<PathInfo>();// 初始图片路径信息队列
    private int index = 0;// id下标

    /**
     * 图片加载工具,默认10线程下载,缓存80张图片
     * 
     * @param context
     */
    public ImageLoad(Context context) {
        this(context, 10, 80, 0);
    }

    /**
     * 图片加载工具
     * 
     * @param context
     *            你懂的
     * @param threadSize
     *            最大线程数
     * @param maxCacheSize
     *            最大缓存图片数量
     * @param defaultImageID
     *            默认图片id
     */
    public ImageLoad(Context context, int threadSize, int maxCacheSize, int defaultImageID) {
        this.context = context;
        this.defaultImageID = defaultImageID;
        executor = Executors.newFixedThreadPool(threadSize);

        loadImagePathInfo();
        // 图片信息数量不足不满最大值,以空白图片信息补足。
        newImagePathInfo(maxCacheSize);
        for (PathInfo pi : original) {
            if (null == pi.url) {
                lost.offer(pi);
            } else {
                use.offer(pi);
                cache.put(pi.url, pi);
            }
        }
        File dir = null;
        if (sdCardExist) {
            dir = new File(Environment.getExternalStorageDirectory() + "/t_image/");
        } else {
            dir = new File(context.getCacheDir() + "/t_image/");
        }

        // 如果文件存在并且不是目录,则删除
        if (dir.exists() && !dir.isDirectory()) {
            dir.delete();
        }
        // 如果目录不存在,则创建
        if (!dir.exists()) {
            dir.mkdir();
        }
    }

    /**
     * 路径信息
     * 
     * @author n.zhang
     * 
     */
    public static class PathInfo {
        private int id;// 图片id 此id用于生成存储图片的文件名。
        private String url;// 图片url
    }

    /**
     * 获得图片存储路径
     * 
     * @param url
     * @return
     */
    public PathInfo getPath(String url) {
        PathInfo pc = cache.get(url);
        if (null == pc) {
            pc = lost.poll();
        }
        if (null == pc) {
            pc = use.poll();
            refresh(pc);
        }
        return pc;
    }

    /**
     * @info 微博使用加载数据路径
     * @author FFMobile-cuihe
     * @date 2012-3-1 下午2:13:10
     * @Title: getsPath
     * @Description: TODO
     * @param@param url
     * @param@return 设定文件
     * @return PathInfo 返回类型
     * @throws
     */

    public PathInfo getsPath(String url) {
        PathInfo pc = cache.get(url);
        if (null == pc) {
            pc = lost.peek();
        }
        // if (null == pc) {
        // pc = use.peek();
        // refresh(pc);
        // }
        return pc;
    }
    public PathInfo getLocalPath(String url) {
        PathInfo pc = cache.get(url);
        if (null == pc) {
            pc = lost.peek();
        }
        return pc;
    }
    /**
     * 刷新路径信息(从索引中删除对应关系、删除对应的图片文件、获取一个新id)
     * 
     * @param pc
     */
    private void refresh(PathInfo pc) {
        long start = System.currentTimeMillis();
        File logFile = null;
        try {
            cache.remove(pc.url);
            File file = toFile(pc);
            file.delete();
            logFile = file;
            pc.id = index++;
            pc.url = null;
        } finally {
            Log.d(TAG_REF, "ref time {" + (System.currentTimeMillis() - start) + "}; ref {" + logFile + "}");
        }
    }

    /**
     * 获得file对象
     * 
     * @param pi
     *            路径缓存
     * @return
     */
    public File toFile(PathInfo pi) {
        if (sdCardExist) {
            return new File(Environment.getExternalStorageDirectory() + "/t_image/" + pi.id + ".jpg");
        } else {
            return new File(context.getCacheDir() + "/t_image/" + pi.id + ".jpg");
        }
    }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param ilCallback
     */
    public void request(String url, final ILCallback ilCallback) {
        final long start = System.currentTimeMillis();
        final PathInfo pc = getPath(url);
        File file = toFile(pc);
        if (null != pc.url) {

            ilCallback.seed(Uri.fromFile(file));
            Log.d(TAG, "load time {" + (System.currentTimeMillis() - start) + "}; cache {" + pc.url + "} ");
        } else {
            pc.url = url;
            Handler mHandler = new Handler() {
                public void handleMessage(Message msg) {
                    if (null == msg.obj) {
                        ilCallback.seed(Uri.EMPTY);
                        Log.d(TAG, "load lost time {" + (System.currentTimeMillis() - start) + "}; network lost {"
                                + pc.url + "}");
                    } else {
                        ilCallback.seed((Uri) msg.obj);
                        Log.d(TAG, "load time {" + (System.currentTimeMillis() - start) + "}; network {" + pc.url + "}");
                    }

                };
            };
            executor.execute(new DownloadImageTask(pc, file, mHandler));
        }
    }

    private void localRequest(String url, final ILCallback ilCallback) {
        final long start = System.currentTimeMillis();
        final PathInfo pc = getLocalPath(url);
        File file = toFile(pc);
        if (null != pc.url) {
            ilCallback.seed(Uri.fromFile(file));
            Log.d(TAG, "load time {" + (System.currentTimeMillis() - start) + "}; cache {" + pc.url + "} ");
        }
    }

    public void localRequest(String url, ImageView iv) {
        localRequest(url, new ImageViewCallback(iv));
    }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    public void request(String url, ImageView iv) {
        request(url, new ImageViewCallback(iv));
    }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    // public void request(String url, ImageButton iv) {
    // request(url, new ImageButtonCallbacks(iv));
    // }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    // public void request(String url, Button iv) {
    // request(url, new ButtonCallbacks(iv));
    // }

    /**
     * 请求加载图片
     * 
     * @param url
     * @param iv
     */
    public void request(String url, ImageSwitcher iv) {
        request(url, new ImageSwitcherCallbacks(iv));
    }

    /**
     * 下载图片任务
     * 
     * @author Administrator
     * 
     */
    private class DownloadImageTask implements Runnable {
        private Handler hc;
        private PathInfo pi;
        private File file;

        public DownloadImageTask(PathInfo pi, File file, Handler hc) {
            this.pi = pi;
            this.file = file;
            this.hc = hc;
        }

        public void run() {
            try {
                byte[] b = requestHttp(pi.url);
                if (null == b) {
                    throw new IOException("数据为空");
                }
                writeFile(file, b);
                use.offer(pi);
                cache.put(pi.url, pi);
                Message message = new Message();
                message.obj = Uri.fromFile(file);
                hc.sendMessage(message);
            } catch (IOException e) {
                Message message = hc.obtainMessage(0, Uri.EMPTY);
                hc.sendMessage(message);
                Log.i(TAG, "image download lost.", e);
            } catch (RuntimeException e) {
                Message message = hc.obtainMessage(0, Uri.EMPTY);
                hc.sendMessage(message);
                Log.i(TAG, "image download lost.", e);
            }
        }
    }

    private void writeFile(File file, byte[] data) throws IOException {
        FileOutputStream out = new FileOutputStream(file);
        try {
            out.write(data);
        } finally {
            out.close();
        }
    }

    private static byte[] requestHttp(String url) throws IOException {
        DefaultHttpClient client = new DefaultHttpClient();
        System.gc();
        try {
            HttpGet get = new HttpGet(url);
            HttpResponse res = client.execute(get);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            if (200 == res.getStatusLine().getStatusCode()) {
                res.getEntity().writeTo(baos);
                return baos.toByteArray();
            } else {
                throw new IOException("httpStatusCode:" + res.getStatusLine().getStatusCode());
            }
        } finally {
            client.getConnectionManager().shutdown();
        }
    }

    /**
     * 读取图片路径信息
     * 
     * @return
     */
    @SuppressWarnings("unchecked")
    private void loadImagePathInfo() {
        long start = System.currentTimeMillis();
        File file = new File(context.getCacheDir() + "/imagePathCache.json");
        try {

            if (!file.isFile()) {
                // 文件不存在。
                Log.d(TAG, "path info file does not exist");
                imageGc();
                return;
            }
            StringWriter sw = new StringWriter();
            char[] buf = new char[1024];
            int len;
            FileReader fr = new FileReader(file);
            while (-1 != (len = fr.read(buf))) {
                sw.write(buf, 0, len);
            }
            fr.close();
            JSONObject json = new JSONObject(sw.toString());
            Iterator<String> it = json.keys();
            while (it.hasNext()) {
                String key = it.next();
                int id = json.getInt(key);
                PathInfo pi = new PathInfo();
                pi.url = key;
                pi.id = id;
                if (index < id) {
                    index = id;
                }
                original.add(pi);
            }
            // 打开文件文件缓存成功
            Log.i(TAG, "load path info ok.");
        } catch (IOException e) {
            Log.i(TAG, "load path info lost - IOException.", e);
            imageGc();
        } catch (JSONException e) {
            Log.i(TAG, "load path info lost - JSONException.", e);
            imageGc();
        } finally {
            if (file.exists()) {
                file.delete();
                Log.d(TAG, "delete path info file");
            }
            Log.d(TAG, "load path info time {" + (System.currentTimeMillis() - start) + "}");
        }

    }

    /**
     * 如果路径信息加载失败,清理图片目录。
     */
    private void imageGc() {
        long start = System.currentTimeMillis();
        try {
            File dir;
            if (sdCardExist) {
                dir = new File(Environment.getExternalStorageDirectory() + "/t_image/");
            } else {
                dir = new File(context.getCacheDir() + "/t_image/");
            }

            if (dir.isDirectory()) {
                for (File file : dir.listFiles()) {
                    file.delete();
                    // gc
                    Log.d(TAG_REF, "gc {" + file + "}");
                }
            }
        } finally {
            // gc 计时
            Log.d(TAG_REF, "gc time {" + (System.currentTimeMillis() - start) + "}");
        }
    }

    private void newImagePathInfo(int max_size) {
        for (int i = original.size(); i < max_size; i++) {
            PathInfo pc = new PathInfo();
            pc.id = index++;
            original.add(pc);
        }
    }

    /**
     * 保存图片路径信息(如记录,下次程序打开,可读取该记录已存图片继续可用)
     */
    public void saveImagePathInfo() {
        long start = System.currentTimeMillis();
        try {
            JSONObject json = new JSONObject();
            for (PathInfo pi : use) {
                try {
                    json.put(pi.url, pi.id);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            File file = new File(context.getCacheDir() + "/imagePathCache.json");
            try {
                FileWriter fw = new FileWriter(file);
                fw.write(json.toString());
                fw.close();
                Log.i(TAG, "image file info save ok.");
            } catch (IOException e) {
                e.printStackTrace();
                Log.i(TAG, "image file info save lost.");
                file.delete();
            }
        } finally {
            Log.d(TAG, "save time {" + (System.currentTimeMillis() - start) + "}");
        }
    }

    /**
     * 图片加载回调
     * 
     * @author n.zhang
     * 
     */
    public static interface ILCallback {
        public void seed(Uri uri);
    }

    private class ImageViewCallback implements ILCallback {
        public ImageViewCallback(ImageView iv) {
            if (defaultImageID > 0) {
                iv.setImageResource(defaultImageID);
            }
            this.iv = iv;
        }

        private ImageView iv;

        public void seed(Uri uri) {
            File f = new File(uri.getPath());
            iv.setImageURI(Uri.parse(f.toString()));
            f = null;
        }
    }

    // private class ImageButtonCallbacks implements ILCallback {
    // public ImageButtonCallbacks(ImageButton iv) {
    // if (defaultImageID > 0) {
    // iv.setBackgroundResource(defaultImageID);
    ////iv.setImageResource(defaultImageID);
    // }
    // this.iv = iv;
    // }
    //
    // private ImageButton iv;
    //
    // public void seed(Uri uri) {
    // iv.setImageURI(uri);
    // }
    // }

    // private class ButtonCallbacks implements ILCallback {
    // public ButtonCallbacks(Button iv) {
    // if (defaultImageID > 0) {
    // iv.setBackgroundResource(defaultImageID);
    ////iv.setImageResource(defaultImageID);
    // }
    // this.iv = iv;
    // }
    //
    // private Button iv;
    //
    // public void seed(Uri uri) {
    // iv.setImageURI(uri);
    // }
    // }

    private class ImageSwitcherCallbacks implements ILCallback {
        public ImageSwitcherCallbacks(ImageSwitcher iv) {
            if (defaultImageID > 0) {
                iv.setImageResource(defaultImageID);
            }
            this.iv = iv;
        }

        private ImageSwitcher iv;

        public void seed(Uri uri) {
            iv.setImageURI(uri);
        }
    }
}
0

It looks as if the problem is actually not in the code you posted. For some reason the stream input is closed. So you are probably closing the stream in image.getPhoto()

M Platvoet
  • 1,679
  • 1
  • 10
  • 14