Курсов проект № 2 по “Интернет програмиране с Java”
Проект 2.1 – Banner Rotator Applet:
Да се напишe Java аплет, който показва кратки скролиращи се рекламни текстови съобщения. При първоначално зареждане аплетът трябва да се свърже със сървъра по протокол TCP на порт 3001 и да издърпа рекламните съобщения. След това трябва да ги показва едно след друго като ги скролира от дясно наляво. Скролирането е подобно на рекламните информационни табла, които се поставят по улиците. Понеже рекламното табло (нашият аплет) е по-малко по размери от едно съобщение, съобщението трябва да се движи (скролира) отдясно наляво и на таблото да се вижда само част от него. Първоначално на таблото няма нищо. В следващият момент на таблото отдясно трябва да започне да се показва началото на съобщението, след това по-голяма част от него, след това още по-голяма част и т.н. Текстът на съобщението плавно, чрез анимация, със скорост достатъчна за да бъде прочетен трябва да се придвижва отдясно наляво. Частите, които излязат от таблото отляво в резултат на движението, трябва да бъдат отрязвани, т.е. да не се виждат, а отдясно трябва да се появяват по-новите части, които досега не са се виждали. Съобщенията трябва да се показват едно след друго в последователността, в която са получени от сървъра. Показването на следващо съобщение трябва да започва веднага след като предходното е излязло изцяло извън рекламното табло, т.е. трябва да имаме следните състояния на рекламната дъска: празна дъска, показване на първото съобщение, празна дъска, показване на второто съобщение и т.н. Нека например имаме две съобщения: “Welcome to our site!” и “New version of our products is available. Check the products page.” В този случай двете съобщения трябва да се показват последователно едно след друго. На картинката се показани 15 примерни изображения от рекламното табло, направени в 15 момента, на кратко времево разстояние един от друг:
След показване на последното съобщение, се преминава отново към първото. Изборът на шрифтовете, цветовете, скоростта на анимацията, рамерът на таблото и всички останали визуални детайли трябва да направите по ваш избор. Ако желаете, можете да добавите и някои специални ефекти с цел “по-красив” външен вид. Ако не можете да направите плавна анимация, не се притеснявайте. Достатъчно е текстът да се движи отдясно наляво, макар и не много плавно, със скорост достатъчна за да се прочете. За получаване на текстовите съобщения вашият аплет трябва да се свързва със следния сървър:
import java.io.*;
import java.net.*;
public class NakovBannerServer
{
public static final int SERVER_PORT = 3001;
public static void sendBannerText(Socket socket)
{
try {
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()) );
out.println("4");
out.println("Welcome to our site!");
out.println("New version of our products is available. " +
"Check the products page.");
out.println("Great discounts! Visit our shop.");
out.println("Please visit the forum.");
out.close();
socket.close();
} catch (IOException ioex) {
ioex.printStackTrace();
}
}
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("Nakov Banner Server is running on port " + SERVER_PORT);
while (true) {
Socket socket = serverSocket.accept();
sendBannerText(socket);
}
}
}
Разбира се, рекламните текстове могат да бъдат други и затова вашият аплет трябва винаги да ги дърпа от сървъра при стартиране. Както се вижда от програмния код, след свързване към сървъра, той изпраща няколко тесктови реда и затваря комуникационния сокет. На първия ред стои едно число N – брой съобщения, а на останалите N реда стои по едно текстово съобщение. Ако аплетът не може по някаква причина да издърпа рекламните съобщения от сървъра, вместо тях той трябва да скролира съобщението “Can not connect to server!”. Считаме, че рекламните съобщения не съръжат кирилица.
Проект 2.2 – Photo Album Applet:
Да се напишe Java аплет, който дава потребителски интерфейс за разглеждане на фотоалбум. Фотоалбумът представлява списък от снимки, всяка от които си има име. Списъкът на достъпните снимки във фотоалбума и самите снимки трябва да се извличат от специален сървър, който е даден по-долу във вид на Java програма. Аплетът трябва да е разделен на 2 части – лява и дясна. В лявата част трябва да се показва списък с имената на достъпните снимки, в който можем да навигираме и да селектираме някое от тези имена. В дясната половина трябва да се показва някоя от снимките или съобщение, че не е заредена снимка или че в момента се зарежда. Аплетът трябва да има и 2 бутона – за зареждане на списъка със снимките и за показване на избраната от списъка снимка. Примерен външен вид на аплета е показан на фигурата:
Първоначално списъкът е празен, а на мястото предназначено за показване на снимките е изобразен текстът “No picture loaded”. При натискане на бутона “Load list”, от сървъра се зарежда списъкът с достъпните снимки, но без самите снимки. След избиране на някоя от снимките и натискане на бутона “Show picture”, съответната снимка се извлича от сървъра и се показва в дясната част на аплета. Снимката трябва да бъде центрирана в областта, предназначена за показването на снимки. Ако снимката е по-голяма от тази област, тя трябва да бъде намалена, така че да се побере, но без да бъде деформирана формата й, т.е. намаляването на размера й трябва да е с еднакъв коефициент по ширина и по височина. Например ако областта за показване на снимките е с размер 300 x 300 пиксела и имаме снимка с размери 512 x 183, то снимката трябва да бъде намалена до размери 300 x 105 и центрирана в тази област (в случая трябва да бъде разположена в правоъгълника с координати (0, 97)-(299,97)-(299,201)-(0,201) относно областта). Забележете, че на фигурата областта за показване на снимката е с няколко пиксела по-малка от контура в дясната част на аплета, но изискването за промяната на размера и центрирането на снимката е спазено. По време на зареждане на снимката от сървъра, в областта, предназначена за нея трябва да се показва съобщението “Loading picture…”. При избиране на друга снимка от списъка с имената на снимките вляво, изобразената снимка вдясно трябва да си остава на мястото и да се заменя с новоизбраната само при натискане на бутона “Show picture”. Евентуалните грешки при комуникацията със сървъра трябва да се обработват адекватно (например чрез показване на подходящо съобщение). Аплетът не трябва да кешира изображенията, тъй като сървърът има право да ги променя по всяко време. Връзката със сървъра трябва да бъде отваряна при стартиране на аплета и затваряна едва при приключвне на работата му. Следва сорса на сървъра, с който вашият аплет трябва да работи:
import java.io.*;
import java.net.*;
public class NakovPhotoAlbumServer
{
public static final int SERVER_PORT = 3002;
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("Nakov Photo Album Server is running on port " +
SERVER_PORT);
while (true) {
Socket socket = serverSocket.accept();
PhotoAlbumClientThread clientThread = new PhotoAlbumClientThread(socket);
clientThread.start();
}
}
}
class PhotoAlbumClientThread extends Thread
{
public static final String PATH = "images/";
public static final String[] fileNames = {
"nakov.jpg", "niki.jpg", "boris.jpg", "raicho.jpg"
};
public static final int BUF_SIZE = 1024;
private Socket mSocket;
private BufferedReader mIn;
private PrintStream mOut;
public PhotoAlbumClientThread(Socket aSocket)
{
mSocket = aSocket;
}
private void doListCommand()
{
mOut.println(fileNames.length);
for (int i=0; i<fileNames.length; i++)
mOut.println(fileNames[i]);
mOut.flush();
}
private void doGetCommand(String fileName)
{
try {
File file = new File(PATH + fileName);
long fileSize = file.length();
FileInputStream fin = new FileInputStream(file);
mOut.println(fileSize);
byte[] buf = new byte[BUF_SIZE];
while (true) {
int bytesRead = fin.read(buf, 0, BUF_SIZE);
if (bytesRead == -1)
break;
mOut.write(buf, 0, bytesRead);
}
fin.close();
} catch (IOException ioex) {
mOut.println(0);
System.err.println("Can not read file " + PATH + fileName);
ioex.printStackTrace();
}
mOut.flush();
}
public void run()
{
try {
mIn = new BufferedReader(
new InputStreamReader(mSocket.getInputStream()));
mOut = new PrintStream(mSocket.getOutputStream());
while (true) {
String command = mIn.readLine();
if (command == null)
break; // connection is broken
if (command.equalsIgnoreCase("LIST"))
doListCommand();
else
if (command.toUpperCase().startsWith("GET"))
doGetCommand(command.substring(4));
}
} catch (IOException ioex) {
ioex.printStackTrace();
}
}
}
Както се вижда от програмния код, сървърът слуша на TCP порт 3002 и изпълнява две команди – LIST, която връща имената на достъпните картинки и GET, която връща заявена картинка от наличните в сървъра. Командата LIST връща списъка в текстов формат – брой картинки N на първия ред и по едно име на картинка на следващите N реда. Командата GET <picture_name> връща заявената картинка в текстово-довичен формат – дължина на файла на първия ред и самия файл, като последователност от байтове на следващия ред. Ако е задано невалидно име на картинка или поради друга причина заявената картинка е нодостъпна, се връща 0 на първия ред. Картинките се връщат във вид на JPG или GIF файл, което позволява лесното им зареждане с метода java.awt.Toolkit.createImage(byte[]). В приложения ZIP файл със сървърите, се намират и изображенията, които са зададени като константа в класа PhotoAlbumClientThread, без които сървърът няма да работи правилно.
Проект 2.3 – Stock Quote Applet:
Да се напише Java аплет, който чертае графика (chart) в реално време отразяваща цените на акциите на борсата на фирма, чийто тикер се въвежда от потребителя. Аплетът трябва да дава възможност на потребителя да въвежда тикер (последователност от 4 главни латински букви) и да наблюдава изменението на цените на акциите на фирмата с този тикер в реално време. За получаване на информацията за цените трябва да се използва специален сървър, който има много прост протокол. При свързване със сървъра, той очаква една текстова линия – тикерът. След въвеждането й, сървърът започва да праща през определен интервал текущата цена на акциите на фирмата със зададения тикер. Цената винаги е реално число между 0 и 999. Сървърът подава през определено време (например през 1 секунда) новата цена във вид на една текстова линия. При получаване на новата цена, аплетът трябва да обнови графиката. Графиката трябва да отразява последните 50 цени, получени от сървъра (разбира се в началото трябва да показва по-малко от 50, поради липса на информация). Аплетът може да изглежда по начин подобен на този:
На лявата картинка е даден аплет, чертаещ графика на цените на акциите на Microsoft (с тикер MFST) 14 секунди след натискане на бутона “Show chart”, а на дясната картинка – след 50 секунди. Обърнете внимание на изискването, че след 50-тата секунда при всяко обновяване цялата графика трябва да се измества наляво с едно деление, за да се освободи място най-вдясно за новата цена, пристигнала от сървъра. Не обръщайте внимание на външния вид на графиката (и на това, че линиите са заоблени чрез интерполация). Дали данните ще са изобразени с начупена линия, някаква крива, последователност от стълбове или по друг начин, не е от значение. Важно е да има някакво графично изображение, което се актуализира при всяко обновление, изпратено от сървъра. Например графиката може да изглежда по начин, подобен на този:
Както вероятно се досещате нашият примерен сървър няма достъп до цените на акциите нито на Nasdaq, нито на някоя друга борса, защото е тестов и защото тази информация обикновено се дава срещу заплащане, но все пак генерира някакви данни, с които можем да си тестваме аплета. Ето и сорс кода на нашия тестов сървър:
import java.io.*;
import java.net.*;
public class NakovStockQuoteServer
{
public static final int SERVER_PORT = 3003;
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("Nakov Stock Quote Server is running on port " +
SERVER_PORT);
while (true) {
Socket socket = serverSocket.accept();
StockQuoteClientThread clientThread = new StockQuoteClientThread(socket);
clientThread.start();
}
}
}
class StockQuoteClientThread extends Thread
{
private static final int TIME_INTERVAL = 1000; // 1 sec.
private Socket mSocket;
private int mOffset = 0;
public StockQuoteClientThread(Socket aSocket)
{
mSocket = aSocket;
}
private double getStockQuote(String aTicker)
{
int initOffset = aTicker.hashCode() % 1000;
int coef = (aTicker.hashCode() % 30 + 1); // coef Î [1..30]
int offset = initOffset + (coef * mOffset);
double angle = ((offset % 1000) * 2*Math.PI) / 999; // angle Î [0..2*PI]
int initValue = (aTicker.hashCode() % 200) + 400; // initValue Î [400..599]
double offsetValue =
(aTicker.hashCode() % 401) * Math.cos(angle); // offsetValue Î [-400..400]
double quote = initValue + offsetValue; // quote Î [0..999]
return quote;
}
public void run()
{
try {
BufferedReader mIn = new BufferedReader(
new InputStreamReader(mSocket.getInputStream()) );
PrintStream mOut = new PrintStream(mSocket.getOutputStream());
String ticker = mIn.readLine();
while (true) {
double quote = getStockQuote(ticker);
mOut.println(quote);
mOffset++;
Thread.sleep(TIME_INTERVAL);
}
} catch (Exception ex) {
// Connection is broken
}
}
}
Както се вижда от кода по-горе, сървърът слуша на TCP порт 3003 и генерира синусуиди, чийто характеристики, зависят от хеш-кода на въведения тикер.Форматът на комуникационният протокол е чисто текстов.
Аплетът трябва да отваря сокет към сървъра и да започва чертане на графиката по данните, получавани от него при всяко натискане на бутона “Show chart”. Сокетът трябва да се затваря, когато вече не е необходим или при завършване на изпълнението на аплета. При възникване на грешка при комуникацията, аплетът трябва да извежда подходящо съобщение.
Понеже аплетите нямат права да отварят сокет към Интернет и могат да комуникират по сокет само със сървъра, от който са заредени, за достъп до отдалечени SMTP сървъри ще използваме специален TCP Proxy сървър, стартиран на машината, от която е зареден аплета. При свързване към този сървър аплетът трябва да изпрати IP адрес и порт на отдалечения сървър, към който иска да отвори сокет във вид на единична текстова линия във формат IP:port. След това TCP Proxy сървърът пренасочва отворения от аплета сокет към зададения сървър и порт. Разликата между директно отворения сокет и сокетът, отворен през TCP Proxy-то, е само при свързването. След свързване, аплетът не може да забележи разлика между директната връзка и пренасочената (минаваща през проксито) връзка, т.е. пренасочването е прозрачно за аплета. По този начин се позволява на аплети да отварят TCP сокети към всяка точка на Интернет. Ето и изходният код на сървъра за пренасочване на TCP трафика, който вашият аплет трябва да използва:
import java.io.*;
import java.net.*;
import java.util.StringTokenizer;
/**
* NakovTCPProxyServer is TCP proxy server designed to provide forward services
* to its clients. When a client is connected, it should send a single line request
* for connection to some remote server in format "server:port" or "IP:port".
* After that NakovTCPProxyServer establishes a connection to the requested server
* and starts forwarding the traffic between the client and this server. All data
* received from the server is sent to the client and all data received from the
* server is sent to the client. So the client can consider that it is connected
* directly to the server and no TCP proxy server is present. NakovTCPProxyServer
* can be used to pass over a firewall or some security permissions. For example
* a Java applet running in a web browser can use this server to open TCP connections
* to the Internet and for example to access POP3 mailboxes on remote server.
*/
public class NakovTCPProxyServer
{
public static final int SERVER_PORT = 3004;
public static void main(String[] aArgs) throws Exception
{
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
System.out.println("Nakov TCP Proxy Server is running on port " +
SERVER_PORT );
while(true) {
Socket socket = serverSocket.accept();
ClientThread clientThread = new ClientThread(socket);
clientThread.start();
}
}
}
/**
* Handles client connections. Reads a pair server:port (as single text line)
* from the client's socket, connects to the specified server and port and does
* data forwarding between the client's socket and specified server in both
* directions ("client in <--> server out" and "server in <--> client out")
* until one of the parties closes the socket or I/O error occurs.
*/
class ClientThread extends Thread
{
private Socket mClientSocket = null;
private Socket mServerSocket = null;
private String mClientHostPort;
private String mServerHostPort;
private boolean mBothConnectionsAreAlive;
public ClientThread(Socket aClientSocket)
{
mClientSocket = aClientSocket;
}
/**
* Obtains a destination server socket to some of the servers in the list
* and starts two threads for forwarding : "client in <--> dest server out"
* and "dest server in <--> client out". After that this thread dies.
*/
public void run()
{
try {
mClientHostPort = mClientSocket.getInetAddress().getHostAddress() +
":" + mClientSocket.getPort();
// Open input and output streams for the client connection
BinaryTextInputStream clientIn =
new BinaryTextInputStream(mClientSocket.getInputStream());
OutputStream clientOut = mClientSocket.getOutputStream();
// Read a server and port from the client socket in format IP:port
mServerHostPort = clientIn.readLine();
// Create a new socket connection to the server specified by the client
mServerSocket = connectToServer();
if (mServerSocket == null) { // If connection can not be established
System.out.println("Can not establish connection to " +
mServerHostPort + " for client " + mClientHostPort + ".");
try { mClientSocket.close(); } catch (IOException e) {}
return;
}
// Open input and output streams for the server connection
BinaryTextInputStream serverIn =
new BinaryTextInputStream(mServerSocket.getInputStream());
OutputStream serverOut = mServerSocket.getOutputStream();
// Start forwarding of socket data between server and client
ForwardThread clientForward =
new ForwardThread(this, clientIn, serverOut);
ForwardThread serverForward =
new ForwardThread(this, serverIn, clientOut);
mBothConnectionsAreAlive = true;
clientForward.start();
serverForward.start();
System.out.println("TCP forwarding " + mClientHostPort +
" <--> " + mServerHostPort + " started.");
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* connectionBroken() method is called by forwarding child threads to notify
* this thread (their parent thread) that one of the connections (server or
* client) is broken (a read/write failure occured). This method disconnects
* both server and client sockets causing both threads to stop forwarding.
*/
public synchronized void connectionBroken()
{
try { mServerSocket.close(); } catch (IOException e) {}
try { mClientSocket.close(); } catch (IOException e) {}
if (mBothConnectionsAreAlive) {
System.out.println("TCP forwarding " + mClientHostPort +
" <--> " + mServerHostPort + " stopped.");
mBothConnectionsAreAlive = false;
}
}
/**
* Reads a server and port from the client socket in format IP:port,
* connects to the specified by the client server and returns the socket
* connection. Returns null if a connection can not be established.
*/
private Socket connectToServer()
{
Socket socket;
try {
StringTokenizer stServerPort = new StringTokenizer(mServerHostPort,": ");
String host = stServerPort.nextToken();
int port = Integer.parseInt(stServerPort.nextToken());
socket = new Socket(host, port);
} catch (Exception ex) {
socket = null;
}
return socket;
}
}
/**
* BinaryTextInputStream is a mixed binary/text input stream.
* It has a binary read(byte[]) method and a text readLine() method.
*/
class BinaryTextInputStream
{
protected InputStream mIn;
public BinaryTextInputStream(InputStream aIn)
{
mIn = aIn;
}
/**
* Reads a text line from the stream.
* @return the line read or null if the end of stream is reached.
* Considers the line finishes with either "\n" or "\r\n".
* @throw IOException in case of I/O problem.
*/
public String readLine()
throws IOException
{
StringBuffer line = new StringBuffer();
while (true) {
int nextByte = mIn.read();
if (nextByte == -1) // EOF
if (line.length()==0)
return null;
else
break;
char ch = (char) nextByte;
if (ch == '\n')
break;
line.append(ch);
}
if ( (line.length()>0) && (line.charAt(line.length()-1)=='\r'))
line.setLength(line.length()-1);
return new String(line);
}
/**
* Delegates to InputStream.read(byte[] b).
*/
public int read(byte[] buf)
throws IOException
{
return mIn.read(buf);
}
}
/**
* ForwardThread handles the TCP forwarding between a socket input stream (source)
* and a socket output stream (destination). It reads the input stream and forwards
* everything to the output stream. If some of the streams fails, the forwarding
* is stopped and the parent thread is notified to close all its connections.
*/
class ForwardThread extends Thread
{
private static final int READ_BUFFER_SIZE = 8192;
private BinaryTextInputStream mInputStream = null;
private OutputStream mOutputStream = null;
private ClientThread mParent = null;
/**
* Creates a new traffic forward thread specifying its input stream,
* output stream and parent thread
*/
public ForwardThread(ClientThread aParent, BinaryTextInputStream aInputStream,
OutputStream aOutputStream)
{
mInputStream = aInputStream;
mOutputStream = aOutputStream;
mParent = aParent;
}
/**
* Runs the thread. Until it is possible, reads the input stream and puts read
* data in the output stream. If reading can not be done (due to exception or
* when the stream is at his end) or writing is failed, exits the thread.
*/
public void run()
{
byte[] buffer = new byte[READ_BUFFER_SIZE];
try {
while (true) {
int bytesRead = mInputStream.read(buffer);
if (bytesRead == -1)
break; // Connection is broken. Exit the thread
mOutputStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
// Read/write failed --> connection is broken --> exit the thread
}
// Notify the parent thread that the connection is broken
// and the forwarding should stop
mParent.connectionBroken();
}
}
Както се вижда от сорс кода, NakovTCPProxyServer слуша на TCP порт 3004.
Изпращането на e-mail трябва да става съгласно SMTP протокола. Е-mail адресите на получателите, въведени в полетата “To email”, “CC” и “BCC” се подават на SMTP сървъра чрез последователни команди “RCPT TO: <email>“. Аплетът трябва да конструира хедъра на писмото по следния образец:
From: "Pesho Peshev" <pesho_peshov@abv.bg>
To: "Svetlin Nakov" <inetjava@nakov.com>
Cc: <niki@top.bg>,
<raicho@egvrn.net>
Subject: Iskam da pitam neshto otnosno 5-ti proekt..
Date: Fri, 12 Apr 2002 16:40:00 +0300
MIME-Version: 1.0
Content-Type: text/plain; charset="cp1251"
<<Message text>>
Забележете, че между хедъра и съдържанието на писмото трябва да има празен ред. Освен това информацията за получателите, които са въведени в полето “BCC” не трябва да присъства нито в хедъра на писмото, нито в тялото му, докато тази от полетата “To email” и “CC” трябва да стои в съответните полета на хедъра. За формат на датата в полето “Date:” използвайте стандартния в Java, т.е. върнатото от израза (new java.util.Date()).toString(). Не забравяйте да ескейпвате редовете, започващи със символа “.” в текста на писмото, съгласно особеностите на протокола. Следете за грешките връщани от сървъра и реагирайте по подходящ начин. Отваряйте сокета към сървъра и през него към SMTP сървъра въведен от потребителя, само при натискане на бутона “SEND” и го затваряйте, веднага след приключване на комуникацията. Attachment-и не трябва да се поддържат. Не е задължително външният вид на вашия аплет да е като показания на фигурата по-горе. Важното е да има описаната функционалност.
Както в предходния проект, понеже аплетите не могат да отварят сокет към Интернет, трябва да се използва NakovTCPProxyServer за свързване с POP3 сървъра, въведен от потребителя. Аплетът може да счита, че този сървър е стартиран на машината, от която той е зареден, на порт 3004.
За всички задачи, които комуникират с някакъв сървър, се отнася, че аплетът трябва да счита, че сървърът е стартиран на компютъра, от който е зареден аплетът, т.е. IP-то му съвпада с IP-то на Web-сървъра.
Всички необходими сървъри са дадени в завършен вид и работят правилно. Разрешава се да нанасяте промени в тях с цел тестване, но само върху данните, до които те осугуряват достъп, без да променяте начина на комуникация с клиента.За стартиране на сървърите, трябва да редактирате по подходящ начин файла startAllServers.bat и да го изпълните.
Всички аплети трябва да имат съответна HTML страница, която ги стартира и трябва да работят под стандартен Web-браузър, който поддържа аплети (например Internet Explorer 5.0).
За разработката на проектите, ако се нуждете от SMTP или POP3 сървър и нямате достъп до такъв, можете да си инсталирате локално сървъра WinProxy, да си направите няколко пощенски кутии посредством административния Web-интерфейс и да си тествате.
Последна версия на този документ можете да намерите от сайта на курса:
http://inetjava.sourceforge.net.
Коментари по задачите можете да публикувате във форума на курса:
http://sourceforge.net/forum/forum.php?forum_id=152811
Моля спазвайте разпределението по проекти, дати и часове, публикувано на сайта!
Автори на проектите: Светлин Наков и Николай Недялков
Последна промяна: 2.05.2002.