Problemik z refleksją & uzyskanie classloadera dla interfejsu z ręki

0

Zrobiłem sobie kiedyś pośrednika przekazującego z JVM zdarzenia zmiany rozdzielczości domyślnego ekranu. Działało to mniej więcej nieźle, tylko z konieczności używało nieładnych importów takich jak sun.java2d.SunGraphicsEnvironment czy sun.awt.DisplayChangedListener. Żeby się trochę zabezpieczyć przed kolejnymi wersjami Javy nie używałem tego bezpośrednio, ale nieco obudowałem, żeby w razie czego musieć zmieniać jak najmniej w tym pośredniku i nic w kodzie, który z niego korzystał.

Ostatnio zachciało mi się wyeliminować nawet te importy, więc zabrałem się za przerobienie na wersję bardziej refleksyjną ;)
Zrobiło się to nieco bardziej skomplikowane niż banalne bo trzeba było zarejestrować listenera na obiekcie GraphicsEnvironment, który oficjalnie tego nie robi, przy pomocy klasy implementującej interfejs, którego oficjalnie nie ma.
O ile wyciągnięcie z obiektu SunGraphicsEnvironment aka GraphicsEnvironment właściwych metod to żaden problem, potknąłem się chyba na wytworzeniu klasy implementującej interfejs sun.awt.DisplayChangedListener. W teorii wiem jak to robić i kiedyś coś nawet robiłem, ale nigdy nie potrzebowałem wyciągnąć class loadera, do interfejsu, z którego mogłem użyć tylko stringa jako nazwy.

Żeby nie było na sucho, poniżej cały kod pośrednika i na końcu dodatkowa klasa okienka z przykładem użycia. Pośrednik, czyli "DisplayListener.To" zawiera zakomentowany wciąż działający kod nie używający refleksji, którego można sobie przełączyć (odkomentować i zakomentować wersje refleksyjne), żeby mieć działające powiadomienia o zmianach grafiki domyślnego ekranu systemu. Zgrzyt w tamtej wersji jest tylko taki kompilator krzyczy, że używa się prywatnych interfejsów Suna, ale poza tym wszystko działa.
Jednak bardziej mnie interesuje wersja z refleksją, która powinna być w przyszłości bardziej "elastyczna".
Oto kod:

//#define REFLECTION
package com.olamagato.video;
import com.olamagato.video.DisplayListener.To;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.SystemColor;
import java.awt.Toolkit;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
//#ifdef REFLECTION
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import javax.swing.SwingUtilities;
//#elif
//import sun.awt.DisplayChangedListener;
//import sun.java2d.SunGraphicsEnvironment;
//#endif

/**
 * Rejestruje zmiany trybu graficznego
 * Powiela sun.awt.DisplayChangedListener dla niewrażliwości
 * od wpływu zmian przyszłych implementacji (sun.awt.*)
 *
 * Wersje od 1.1 używają refleksji.
 * @author Olamagato
 * @version 1.1
 * @since 2006
 */
public interface DisplayListener
{
	/**
	 * Informuje o zmianie rozdzielczości ekranu
	 */
	void displayChanged();
	/**
	 * Informuje o zmianie palety kolorów ekranu
	 */
	void paletteChanged();

	/**
	 * Intefejs pozwalający zmieniać odbieranie zdarzeń zmian grafiki.
	 */
	interface Switch
	{
		/**
		 * Włącza odbieranie zmian.
		 * @return true jeżeli udało się włączyć odbieranie zdarzeń
		 */
		boolean enable();

		/**
		 * Wyłącza odbieranie zmian.
		 * @return true jeżeli udało się wyłączyć odbieranie zdarzeń
		 */
		boolean disable();
	} //interface Switch

	/**
	 * Pośredniczy w informowaniu o zmianach trybu graficznego z
	 * GraphicsEnvironment jeżeli jest on typu SunGraphicsEnvironment.
	 * Włącza i wyłacza informowanie o zmianach.
	 */
	@SuppressWarnings("UseOfSystemOutOrSystemErr")
	class To implements Switch
	{
		/**
		 * Tworzy pośrednika do docelowego listenera.
		 * @param listener klasa implementująca DisplayListener
		 */
		public To(DisplayListener listener)
		{
			if(listener == null)
				throw new IllegalArgumentException("listener = null");
			this.listener = listener;
			debugNullCheck("listener", this.listener);
			this.ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
			debugNullCheck("ge", this.ge);
			initReflection();
		}

//#ifndef REFLECTION

//		/**
//		 * Rejestruje obserwatora dla zmian trybu graficznego
//		 * Nadmiarowe rejestracje są ignorowane.
//		 */
//		@Override public synchronized boolean enable()
//		{
//			if(listener == null) return false;
//			if(!registered && ge instanceof SunGraphicsEnvironment)
//			{
//				final SunGraphicsEnvironment sge = (SunGraphicsEnvironment)ge;
//				sge.addDisplayChangedListener(speaker);
//				registered = true;
//			}
//			return registered;
//		}
//
//		/**
//		 * Wyrejestrowuje obserwatora zmian trybu graficznego
//		 * Nadmiarowe wyrejestrowania są ignorowane.
//		 */
//		@Override public synchronized boolean disable()
//		{
//			if(listener == null) return false;
//			if(registered && ge instanceof SunGraphicsEnvironment)
//			{
//				final SunGraphicsEnvironment sge = (SunGraphicsEnvironment)ge;
//				sge.removeDisplayChangedListener(speaker);
//				registered = false;
//			}
//			return registered;
//		}

//#elif

		/**
		 * Rejestruje obserwatora dla zmian trybu graficznego
		 * Nadmiarowe rejestracje są ignorowane.
		 * @return true jeżeli listener został/jest zarejestrowany
		 */
		@Override public synchronized boolean enable()
		{
			System.out.println("Próba włączenia listenera grafiki...");
			debugNullCheck("listener", listener);
			if(listener == null) return false;
			else if(registered)
			{
				System.out.println("Listener był zarejestrowany.");
				return true;
			}
			else if(hasMethod(ge, addDisplayChangedListener, false))
			{
				System.out.println("1:Grafika gotowa do rejestracji...");
				try { addDisplayChangedListener.invoke(ge, speaker); }
				catch(IllegalAccessException | IllegalArgumentException
					| InvocationTargetException ex)
				{
					System.out.println("2:Nie zarejestrowano listenera!\n"
						+ ex.toString());
					return false;
				}
				registered = true;
				System.out.println("3:Zarejestrowano listenera grafiki.");
				return true;
			}
			else System.out.println("4:Brak metody addDisplayChangedListener!");
			return false;
		}
		/**
		 * Wyrejestrowuje obserwatora zmian trybu graficznego
		 * Nadmiarowe wyrejestrowania są ignorowane.
		 * @return true jeżeli listener jest niezarejestrowany
		 */
		@Override public synchronized boolean disable()
		{
			System.out.println("Próba wyłączenia listenera grafiki...");
			debugNullCheck("listener", listener);
			if(listener == null) return false;
			else if(!registered)
			{
				System.out.println("Listener nie był zarejestrowany.");
				return true;
			}
			else if(hasMethod(ge, removeDisplayChangedListener, false))
			{
				System.out.println("5:Grafika gotowa do wyrejestrowania...");
				try { removeDisplayChangedListener.invoke(ge, speaker); }
				catch(IllegalAccessException | IllegalArgumentException
					| InvocationTargetException ex)
				{
					System.out.println("6:Nie wyrejestrowano listenera!");
					return false;
				}
				registered = false;
				System.out.println("7:Wyrejestrowano listenera grafiki.");
				return true;
			}
			else System.out.println("8:Brak metody"
					+ " removeDisplayChangedListener!");
			return false;
		}

//#endif

		//pozwala na wielokrotne rejestracje bez ryzyka wykrzaczenia
		private boolean registered;
		//odnośnik do klasy powiadamianej o zmianach
		private DisplayListener listener;
		//Zachowuje stan środowiska gdyby nie był już dostępny
		//w czasie wyrejestrowywania
		private GraphicsEnvironment ge;

//#ifdef REFLECTION

		////// Reflection //////

		private static boolean debugNullCheck(String name, Object object)
		{
			final boolean notNull = object != null;
			System.out.println(name + (notNull ? " != " : " == ") + "NULL");
			return notNull;
		}

		/**
		 * Sprawdza czy obiekt zawiera metodę find przez uzyskanie dostępu
		 * do nazw wszystkich metod i porównanie par występujących argumentów.
		 * @param checked sprawdzany obiekt
		 * @param find metoda do wyszukania
		 * @param declared true jeżeli wyszukane mają być wszystkie metody
		 * @return true jeżeli obiekt implementuje szukaną metodę
		 */
		private static boolean hasMethod(Object checked, Method find,
			boolean declared)
		{
			Class<?> c = checked.getClass();
			String findName = find.getName();
			Class<?>[] findParams = find.getParameterTypes();
			for(Method next: declared ? c.getDeclaredMethods() : c.getMethods())
				if(next.getName().equals(findName)
					&& Arrays.equals(next.getParameterTypes(), findParams))
						return true;
			return false;
		}

		private void initReflection()
		{
			try
			{
				displayChangedListenerClass =
					Class.forName("sun.awt.DisplayChangedListener");
				debugNullCheck("displayChangedListenerClass",
					displayChangedListenerClass);
				ClassLoader listenerClassLoader = //NULL?
					displayChangedListenerClass.getClassLoader();
				debugNullCheck("listenerClassLoader", listenerClassLoader);
				speaker = Proxy.newProxyInstance(listenerClassLoader,
					new Class<?>[] { displayChangedListenerClass },
					new InvocationHandler()
					{
						@Override public Object invoke(Object proxy,
							Method method, Object[] args) throws Throwable
						{
							final String methodName = method.getName();
							if(method.getParameterTypes().length == 0)
								switch(methodName)
								{
								case "displayChanged":
									listener.displayChanged(); break;
								case "paletteChanged":
									listener.paletteChanged(); break;
								}
							return null; //void
						}
					}); ///*DisplayChangedListener*/ Object speaker
				debugNullCheck("speaker", speaker);

				addDisplayChangedListener =	ge.getClass()
					.getMethod("addDisplayChangedListener",
					displayChangedListenerClass);
//				addDisplayChangedListener.setAccessible(true);
				debugNullCheck("addDisplayChangedListener",
					addDisplayChangedListener);

				removeDisplayChangedListener = ge.getClass()
					.getMethod("removeDisplayChangedListener",
					displayChangedListenerClass);
//				removeDisplayChangedListener.setAccessible(true);
				debugNullCheck("removeDisplayChangedListener",
					removeDisplayChangedListener);
			} //try
			catch(ClassNotFoundException | NoSuchMethodException
				| SecurityException ex)
			{
				System.out.println("9:Inicjacja refleksji nieudana.");
				Logger.getLogger(DisplayListener.class.getName()).
					log(Level.SEVERE, null, ex);
				throw new RuntimeException(ex);
			}
		} //init

		private Class<?> displayChangedListenerClass;
		private Method addDisplayChangedListener;
		private Method removeDisplayChangedListener;
		private /*DisplayChangedListener*/ Object speaker;

//#elif

//		private DisplayChangedListener speaker =
//			new DisplayChangedListener()
//		{
//			/**
//			 * Wywoływana tylko przez sun.awt.DisplayChangedListener
//			 */
//			@Override public void displayChanged()
//				{ listener.displayChanged(); }
//			/**
//			 * Wywoływana tylko przez sun.awt.DisplayChangedListener
//			 */
//			@Override public void paletteChanged()
//				{ listener.paletteChanged(); }
//		};

//#endif

		public static void main(String[] args)
			{ SwingUtilities.invokeLater(new TestZmianyGrafiki()); }
	} //class DisplayListener.To implements Switch
} //public interface DisplayListener

@SuppressWarnings(
	{"UseOfSystemOutOrSystemErr", "MultipleTopLevelClassesInFile"} )
class TestZmianyGrafiki
	extends WindowAdapter implements DisplayListener, Runnable
{
	public TestZmianyGrafiki()
	{
		frame = new JFrame();
		text = new JTextArea();
		listening = new DisplayListener.To(this);
	}

	@Override public void run()
	{
		try { UIManager.setLookAndFeel(
			UIManager.getSystemLookAndFeelClassName()); }
		catch(UnsupportedLookAndFeelException | ClassNotFoundException |
			InstantiationException | IllegalAccessException e) {}
		final Font f = text.getFont();
		text.setFont(f.deriveFont(f.getSize2D() * 2));
		text.setBackground(SystemColor.desktop);
		text.setForeground(SystemColor.windowText);
		text.setText(changed("Stan początkowy"));
		text.setEditable(false);
		frame.setTitle("Rejestrator zmiany grafiki");
		frame.setPreferredSize(new Dimension(712, 400));
		frame.getContentPane().add(new JScrollPane(text));
		frame.pack();
		frame.setLocationRelativeTo(null);
//		frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
		//bez automatycznego windowClosed|JVM exit
		frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		frame.addWindowListener(this);
		frame.setVisible(true);
		listening.enable();
		System.out.println("Oczekiwanie na zmiany rozdzielczości/palety...\n");
	}

	@Override public void windowClosing(WindowEvent e)
	{
		System.out.println("Zamykanie okna...");
		if(listening.disable())
		{
			frame.setVisible(false); //nadmiarowe dla dispose()
			frame.dispose(); //odpalenie windowClosed via GUI
		}
	}

	@Override public void windowClosed(WindowEvent e)
	{
		System.out.println("Zamknięto okno...");
		frame.removeWindowListener(this);
	}

	@Override public void displayChanged() { show(HEAD); }

	@Override public void paletteChanged() { show(HEAD); }

	private void show(final String head) { text.append(changed(head)); }

	private static String changed(final String text)
	{
		final Toolkit t = Toolkit.getDefaultToolkit();
		final Dimension scr = t.getScreenSize();
		final int bits = t.getColorModel().getPixelSize();
		return String.format("%s: %s%nWymiary: %d x %d, "
			+ "rozmiar piksela (wg JVM): %d-bit%n", toStr(new Date()),
			text, scr.width, scr.height, bits);
	}

	private static String toStr(Date time)
	{
		return DateFormat.getTimeInstance(DateFormat.MEDIUM)
			.format(time);
	}

	private static final String HEAD = "Zmieniono tryb graficzny";
	private JFrame frame;
	private JTextArea text;
	private To listening;
} //class TestZmianyGrafiki implements DisplayListener

Logi do System.out dają coś takiego (debug podobnie):

listener != NULL
ge != NULL
displayChangedListenerClass != NULL
listenerClassLoader == NULL
speaker != NULL
addDisplayChangedListener != NULL
removeDisplayChangedListener != NULL
Próba włączenia listenera grafiki...
listener != NULL
1:Grafika gotowa do rejestracji...
2:Nie zarejestrowano listenera!
java.lang.reflect.InvocationTargetException
Oczekiwanie na zmiany rozdzielczości/palety...

Zamykanie okna...
Próba wyłączenia listenera grafiki...
listener != NULL
Listener nie był zarejestrowany.
Zamknięto okno...

Jak widać displayChangedListenerClass wydaje się ok, ale problem robi listenerClassLoader, który jest nullem dzięki czemu tworzony na jego podstawie obiekt
speaker = Proxy.newProxyInstance(...), też właściwie dostaje null (czego nie bardzo widać).
Cała reszta operacji nie może być udana nie mogąc zarejestrować oryginalnego listenera.

Ma ktoś z was jakiś pomysł jak się dorwać do tego poprawnego classloadera? Ewentualnie gdzie jeszcze coś skopałem.

0

Szczerze? TL; DR. Tyle pouczania o tym jak pisac posty, a jeden z najstarszych stazem userow pizga takie kobyly...

Nie wiem czy to Ci pomoze, ale classloader bedacy nullem to jest tzw. bootstrap classloader. javadoc dla getClassLoader() mowi:

Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.

Czyli Twoj przypadek?
Co sie dzieje jesli sprobujesz to zrobic tak:
ClassLoader.getSystemClassLoader()?
Powinienes costam dostac co nie jest nullem, i probujac wczytac klase tym CL powinien najpierw sie odwolac do bootstrapa i klase powinienes dostac tak czy tak.

0

Nikt nie zmusza Cię do czytania kodu. Problem opisałem we wstępie. Kod był tylko dodatkiem likwidującym zbędne dodatkowe pytania.
Tematu dotychczas nie ruszałem (prawie nic nie robiłem z użyciem refleksji), a kawałek javadoca, który przytoczyłeś nic mi nie mówił. Bo to, że coś "może", to informacja warta mniej niż zero. W każdym razie dzięki za info bo już się brałem za szukanie rozwiązania w innej wersji forName (bez inicjowania czy coś).
Nie przychodziło mi też do głowy, że mogę sobie skakać po różnych loaderach, uzyskiwanych skądinąd. Jeszcze nie zaakceptowałem do końca koncepcji loadera do interfejsu, który kodu jest przecież pozbawiony, ale chyba muszę więcej powęszyć w javadocu.

0

Nie obraz sie, ale jesli getClassLoader() zwraca nulla, i masz w Javadocu napisane ze dla bootstrapa moze to byc null, i nadal nie rozumiesz skad null, to moze powinienes zajac sie czym innym? Moze dla ciebie informacja jest warta mniej niz zero; tym niemniej, jest ona prawdziwa i jest odpowiedzia na zadane pytanie. Moze po prostu nie rozumiesz po angielsku?
Jesli 'skakanie po loaderach' to model parent-child i delegacja do parenta jako pierwszy krok ladowania klasy, to faktycznie masz duzo do nadrobienia jesli chodzi o refleksje, oraz o classloading w Javie ogolnie.
Interfejs wg. ciebie nie ma kodu? To ze metody nie maja implementacji nie znaczy, ze plik .class nie ma bytecodu, ktory JVM musi zaladowac. Faktycznie poczytaj troche dokumentacji.

0

Wybacz, ale z informacji, że szczególny przypadek spowodować zwrot nulla nie wynika, że dostając nulla masz na pewno ten szczególny przypadek. To tylko jedna z możliwości. Implikacja działa tylko w jedną stronę - tak mnie kiedyś na studiach uczyli.

Co do tematu - jak na razie radziłem sobie ze wszystkimi problemami na jakie się natykałem, praktycznie bez potrzeby korzystania z forum (wygooglować idzie nawet numer komórki kierowcy śmieciarki, która właśnie odjechała spod domu.;)
Raz na rok zdarza się, że dostaję lenia i wydaje mi się, że od kogoś mogę uzyskać odpowiedź łatwiej lub szybciej niż grzebanie i wgryzanie się w temat do bólu. Tym bardziej przy tak banalnym problemie, który ruszyłem z doskoku.
Co do Javy to tę trochę znam (język), ale nie znaczy to, że dobrze znam wszystkie technologie, biblioteki, JVM i wszystkie mechanizmy jakie w niej istnieją. A już szczególnie refleksję, którą używa się podobnie "często" jak wstawek w asemblerze w C++.
Popełniania pomyłek, ani braku jakiejś szczegółowej wiedzy - nie wstydzę się bo to żaden powód do wstydu. Po to właśnie jest forum - czego mimo zapewne sporej wiedzy - najwyraźniej nie wiesz.
Pozdrawiam.

ps. Tak czy inaczej jeżeli ktoś ma działający przykład implementacji listenerów (lub czegoś jeszcze bardziej wykombinowanego, takiego jak np. użycie klas wewnętrznych) wyłącznie przy pomocy refleksji, to prosiłbym o podrzucenie. W podręcznikach jest o refleksji i niuansach działania JVM tyle tematów, co kot napłakał.

0

Prawde mowiac nie wiem o co Ci chodzi. Pytanie bylo jak dobrac sie do poprawnego CL - null jest poprawny, bo to jest bootstrap, masz odpowiedz. Zeby sprawdzic co masz jeszcze zle podales po prostu zdecydowanie za wiele kodu jak na moj gust, poki co nikomu tutaj nie chcialo sie go analizowac - do tego odnosil sie moj TL; DR.
Dla testu - uruchom aplikacje i wez CL z jakiejs Twojej klasy ktora jest w classpath - pewnie bedzie to jakis URLClassLoader lub Launcher czy cokolwiek, ale nie null. Teraz, uruchom aplikacje jeszcze raz, dodajac te klasy do bootstrapa za pomoca -Xbootclasspath/a (append) lub -Xbootclasspath/p (prepend) - bedzie nullem, bo wczyta ja natywny bootstrap classloader. Mechanizm dosc dobrze znany.
Zajmujesz sie dosc skomplikowanymi rzeczami (refleksja, cl, hackowanie klas specyficznych dla JVM Oracla itp.), ale z drugiej stony marudzisz ze 'moze byc null' nie jest nic warte. Ok. Nie wiem jednak, w jaki sposob chcesz hakowac specyficzne dla implementacji JVM mechanizmy nie wiedziac i negujac jak one dzialaja. Na tym to polega - hakujesz Oracle JVM (nie oczukujesz chyba ze JVM IBMa bedzie miec pakiet sun.* czy cokolwiek tam uzywasz?), wiec musisz poznac jego mechanizmy. bootstrap CL bedacy null jest jednym z nich, i nic na to nie poradzisz. Sie nie podoba, sie mowi trudno i zostawia w spokoju.
Jako przyklad biblioteki ktora hakuje rozne JVM moge podac Objnesis - tworzenie obiektow bez wywolywania konstruktora, z ktorej korzysta np. Mockito. Kazda JVM ktora wspieraja ma specyficzne mechanizmy, ktore w specyfikacji sa okreslane slowami 'should' lub 'might' lub innymi, czyli sa nieprzenosne i specyficzne dla implementacji. Tym niemniej, nie marudza i ladnie to zenkapsulowali (wiem, fajne slowo).

0

Co do tego jak Cie uczyli na matematyce implikacji - teraz juz szkole skonczyles, i teoria teoria, ale tutaj chodzi o 'get things done' - trzeba troche eksperymentowac, poznawac mechanizmy, testowac. Zreszta nie wiem gdzie masz tam implikacje.

0

OK. To wyjaśnię bo może niejasno wytłumaczyłem co jest moim problemem. Otóż potrzebuję takiego class loadera dla proxy, który powinien mi pozwolić uzyskać klasę konkretną implementującą interfejs sun.awt.DisplayChangedListener, którego muszę uzyskać wyłącznie z palca (nie mogę się odwołać do typu w czasie kompilacji).
Na początku metody initReflection jest taki, jak sądzę kluczowy początek kodu, który mam źle napisany:

	ClassLoader listenerClassLoader = //NULL?
		displayChangedListenerClass.getClassLoader();
		speaker = Proxy.newProxyInstance(listenerClassLoader,
		new Class<?>[] { displayChangedListenerClass },
		new InvocationHandler() {...}

Druga linijka bierze sobie de facto nulla, a to powoduje, że wytworzona za jego pomocą Proxy jest bezwartościowa (obiekt z nullami w środku, choć sama jest rzekomo dobrze wyprodukowana).
Moim pytaniem było jak uzyskać taki obiekt classloader, który pozwoli mi na pewno uzyskać klasę Proxy. A tu warunkiem koniecznym jest jak się wydaje classloader != null. Krótko mówiąc to czego na pewno nie chcę to zadziałania bootstrapa bo wtedy mam nulla, który jest dla mnie wykluczony.

Co do hakowania, to doskonale wiem, że na jakiejś open czy exotic jvm ten kawałek kodu nie będzie mi działał. Nie będzie mi działał również wtedy kiedy Oracle postanowi wywalić ten kod z jego JDK. Tyle, że w każdym z tych przypadków (i dowolnego OSa) mogę dopisać kawałeczek kodu, który będzie to robił dla każdego przypadku trochę inaczej, ale ostatecznie kod klienta, który z niego skorzysta nie będzie różnił się nic. No chyba, że Oracle w końcu upubliczni listnera do zmiany rozdzielczości i wtedy cały ten kod pójdzie do śmietnika (nawet jeżeli będzie działać). Ale w międzyczasie czego się o refleksji nauczę, to moje.
Pozdrawiam.

ps. Jeszcze nie zdążyłem sprawdzić Twojej odpowiedzi w pierwszym poście, więc możliwe, że już mi pomogłeś.
Ale to będę dopiero sprawdzał.

1 użytkowników online, w tym zalogowanych: 0, gości: 1